716 lines
28 KiB
JavaScript
716 lines
28 KiB
JavaScript
/* 二维工程图与分步装配向导 */
|
||||
|
|
/* =========================================================================
|
|||
|
|
交互式装配图与分步装配向导引擎 (Interactive Assembly Drawing Engine)
|
|||
|
|
========================================================================= */
|
|||
|
|
let sheetInited = false;
|
|||
|
|
let curSheetIdx = 0; // 0: front, 1: joint, 2: exploded, 3: side, 'guide': assembly guide
|
|||
|
|
let curSheetTheme = 'paper';
|
|||
|
|
let sheetScale = 1.0;
|
|||
|
|
let sheetPanX = 0, sheetPanY = 0;
|
|||
|
|
let isSheetDragging = false;
|
|||
|
|
let sheetDragStartX = 0, sheetDragStartY = 0;
|
|||
|
|
let pinnedPartIdx = null;
|
|||
|
|
let currentGuideStep = 0;
|
|||
|
|
|
|||
|
|
const SHEET_METAS = [
|
|||
|
|
{ id: 'front', title: '01 总装图 - 正视', icon: '📐', desc: '沿 -X 投影;肢体在纸面内,显示左右交替堆叠。全部贴合面共用 Y=157 平面。' },
|
|||
|
|
{ id: 'joint', title: '02 回转副 A-A 剖视', icon: '🔍', desc: '回转副局部放大 6 倍 — 实测数值基准:端面齿联轴器、对开抱箍与从动盘。' },
|
|||
|
|
{ id: 'exploded', title: '03 轴测爆炸装配图', icon: '💥', desc: '轴测爆炸展开:逐级装配顺序,电机与端面齿组件沿轴线抽出。' },
|
|||
|
|
{ id: 'side', title: '04 总装图 - 侧视', icon: '📐', desc: '沿 +Y 投影;俯仰轴垂直纸面。肩抬举偏置 92mm,肩吊臂侧板无干涉。' }
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const ASSEMBLY_STEPS = [
|
|||
|
|
{
|
|||
|
|
step: 1,
|
|||
|
|
title: '基座与三段立柱总装',
|
|||
|
|
subtitle: '底脚锁紧 · 内部中空走线孔道贯通',
|
|||
|
|
desc: '将 P02 Center/Front/Rear 底板组合锁紧,插入三段分体打印的 P01 立柱(A下段、B中段、C上段)。立柱段间通过止口插紧,内腔预留 Ø28mm 走线孔道,保证后续主干总线平顺穿入。',
|
|||
|
|
parts: [
|
|||
|
|
{ idx: 4, name: 'P02_Center 立柱锁紧底盘', role: '承重基板' },
|
|||
|
|
{ idx: 5, name: 'P02_Front 前支承脚', role: '防前倾支撑' },
|
|||
|
|
{ idx: 6, name: 'P02_Rear 后配重脚', role: '配重稳定' },
|
|||
|
|
{ idx: 1, name: 'P01_A 立柱下段', role: '立柱下部' },
|
|||
|
|
{ idx: 2, name: 'P01_B 立柱中段', role: '立柱中部' },
|
|||
|
|
{ idx: 3, name: 'P01_C 立柱上段', role: '立柱顶部与J1过渡' }
|
|||
|
|
],
|
|||
|
|
screws: [
|
|||
|
|
{ spec: 'M5 x 25 (S07)', qty: 4, role: '底盘主定位紧固' },
|
|||
|
|
{ spec: 'M5 x 25 (S08)', qty: 4, role: '前后支承脚拼合' },
|
|||
|
|
{ spec: 'M6 x 35 (S10)', qty: 6, role: '重载沉孔锚固' },
|
|||
|
|
{ spec: 'M4 x 16 (S05)', qty: 8, role: '立柱三段段间自攻插合' }
|
|||
|
|
],
|
|||
|
|
tips: '安装前核对立柱内部走线通道无打印拉丝残留,螺丝按对角线分次拧紧。',
|
|||
|
|
sheetRef: 0,
|
|||
|
|
camera: { az: -0.6, po: 1.15, rd: 550, tx: 0, ty: 220, tz: 350 }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
step: 2,
|
|||
|
|
title: '肩部双自由度关节总成 (J1 & J2)',
|
|||
|
|
subtitle: '肩外展抬举 + 肩俯仰正交布置',
|
|||
|
|
desc: '在立柱顶端固定 P03 肩部关节安装架,装入 J1 伺服电机;通过端面齿联轴器连接 P04 正交肩肘;装入 J2 俯仰电机,形成沿 X 轴偏置 92mm 的正交双关节。',
|
|||
|
|
parts: [
|
|||
|
|
{ idx: 7, name: 'P03 肩部关节安装架', role: '肩部主承力座' },
|
|||
|
|
{ idx: 14, name: 'M1_J1 电机模组', role: '肩抬举驱动' },
|
|||
|
|
{ idx: 8, name: 'P04 正交肩肘', role: '90°正交转接' },
|
|||
|
|
{ idx: 15, name: 'M2_J2 电机模组', role: '肩俯仰驱动' }
|
|||
|
|
],
|
|||
|
|
screws: [
|
|||
|
|
{ spec: 'M2 x 10 (S01)', qty: 6, role: '电机输出端面固定' },
|
|||
|
|
{ spec: 'M3 x 10 (S02)', qty: 8, role: '电机机座轴向夹紧' },
|
|||
|
|
{ spec: 'M3 x 8 (S04)', qty: 4, role: '端面齿防脱抱箍锁紧' },
|
|||
|
|
{ spec: 'M3 x 16 (S03)', qty: 4, role: 'P03 顶盖贯穿紧固' }
|
|||
|
|
],
|
|||
|
|
tips: '抱箍安装前务必确认两半端面齿盘脸已 100% 贴死,抱箍两半对开缝隙均匀。',
|
|||
|
|
sheetRef: 1,
|
|||
|
|
camera: { az: -0.8, po: 1.25, rd: 420, tx: -15, ty: 120, tz: 550 }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
step: 3,
|
|||
|
|
title: '大臂旋转座与主受力骨架 (J3 & P05/P06)',
|
|||
|
|
subtitle: '大臂内旋自由度 · 桁架式轻量化骨架',
|
|||
|
|
desc: '将 P05 大臂旋转座与 J2 输出端面贴合,装入 J3 大臂旋转电机;连接 P06 大臂骨架。P06 采用箱梁减重设计,侧壁开设有主总线走线过孔与扎带绑扎凸台。',
|
|||
|
|
parts: [
|
|||
|
|
{ idx: 9, name: 'P05 大臂旋转座', role: 'J3旋转基座' },
|
|||
|
|
{ idx: 16, name: 'M3_J3 电机模组', role: '大臂自旋驱动' },
|
|||
|
|
{ idx: 10, name: 'P06 大臂骨架', role: '大臂主连杆 (L1=158mm)' }
|
|||
|
|
],
|
|||
|
|
screws: [
|
|||
|
|
{ spec: 'M2 x 10 (S01)', qty: 3, role: 'J3 输出法兰固定' },
|
|||
|
|
{ spec: 'M3 x 10 (S02)', qty: 4, role: 'J3 外壳压紧' },
|
|||
|
|
{ spec: 'M3 x 8 (S12)', qty: 4, role: 'P06 侧向加强壁锁紧' }
|
|||
|
|
],
|
|||
|
|
tips: 'J3 尾部排线由 P05 侧边专用豁口引出,避免受力压线或磨破外绝缘皮。',
|
|||
|
|
sheetRef: 0,
|
|||
|
|
camera: { az: -0.7, po: 1.2, rd: 380, tx: 20, ty: 100, tz: 410 }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
step: 4,
|
|||
|
|
title: '肘部俯仰关节与前臂驱动 (J4 & P07)',
|
|||
|
|
subtitle: '肘关节俯仰驱动 · 偏置布置消除干涉',
|
|||
|
|
desc: '在大臂末端装入 J4 肘部电机,连接 P07 小臂骨架。小臂骨架向上加高保护电机,侧面设置 U 型过关节线束缓冲槽,确保大角度折叠时总线不紧绷。',
|
|||
|
|
parts: [
|
|||
|
|
{ idx: 17, name: 'M4_J4 电机模组', role: '肘关节俯仰驱动' },
|
|||
|
|
{ idx: 11, name: 'P07 小臂骨架', role: '前臂连杆 (L2=110mm)' }
|
|||
|
|
],
|
|||
|
|
screws: [
|
|||
|
|
{ spec: 'M2 x 10 (S01)', qty: 3, role: 'J4 输出法兰' },
|
|||
|
|
{ spec: 'M3 x 10 (S02)', qty: 4, role: 'J4 抱紧沉孔' },
|
|||
|
|
{ spec: 'M3 x 8 (S12)', qty: 4, role: 'P07 顶部补强板锁紧' }
|
|||
|
|
],
|
|||
|
|
tips: '检查肘关节在全行程运动范围内线束无卡死或拉扯。',
|
|||
|
|
sheetRef: 3,
|
|||
|
|
camera: { az: -0.6, po: 1.2, rd: 320, tx: 20, ty: 65, tz: 330 }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
step: 5,
|
|||
|
|
title: '腕部双自由度与末端工具法兰 (J5, J6, P08, P09)',
|
|||
|
|
subtitle: '腕部俯仰与自转 · 标准 ISO 工具接口',
|
|||
|
|
desc: '在小臂末端装入 J5 腕部俯仰电机,连接 P08 腕部转接座;装入 J6 末端自旋电机,前端安装 P09 工具法兰。法兰带有标准 PCD 孔位,适配电动夹爪或快换工具。',
|
|||
|
|
parts: [
|
|||
|
|
{ idx: 18, name: 'M5_J5 电机模组', role: '腕俯仰驱动' },
|
|||
|
|
{ idx: 12, name: 'P08 腕部转接座', role: '腕偏转过渡' },
|
|||
|
|
{ idx: 19, name: 'M6_J6 电机模组', role: '末端自转驱动' },
|
|||
|
|
{ idx: 13, name: 'P09 末端工具法兰', role: '标准夹爪安装面' }
|
|||
|
|
],
|
|||
|
|
screws: [
|
|||
|
|
{ spec: 'M2 x 10 (S01)', qty: 6, role: 'J5/J6 输出法兰' },
|
|||
|
|
{ spec: 'M3 x 10 (S02)', qty: 8, role: 'J5/J6 机座固定' },
|
|||
|
|
{ spec: 'M4 x 16 (S06)', qty: 4, role: '末端工具安装紧固' }
|
|||
|
|
],
|
|||
|
|
tips: 'J6 法兰盘面与电机旋转中心精密对齐,端面螺钉交替预紧。',
|
|||
|
|
sheetRef: 2,
|
|||
|
|
camera: { az: -0.6, po: 1.25, rd: 240, tx: 20, ty: 80, tz: 200 }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
step: 6,
|
|||
|
|
title: '电气线束主干铺设与扎带固定',
|
|||
|
|
subtitle: '4芯高柔拖链电缆 · 各关节U型缓冲弯 · 防拉扯扎带',
|
|||
|
|
desc: '将 4 芯总线(2×CAN 通信 + 2×24V 供电)从立柱内腔穿入,经各关节 T 接小板向 J1~J6 分支。在各零件外壳的专用扎带固定槽使用阻燃尼龙扎带锁紧,过关节处保留 35mm U 型自由弯曲余量。',
|
|||
|
|
parts: [
|
|||
|
|
{ idx: 14, name: 'J1 电机总线分支', role: '首端分支' },
|
|||
|
|
{ idx: 15, name: 'J2 电机总线分支', role: '肩部供电' },
|
|||
|
|
{ idx: 16, name: 'J3 电机总线分支', role: '大臂供电' },
|
|||
|
|
{ idx: 17, name: 'J4 电机总线分支', role: '肘部供电' },
|
|||
|
|
{ idx: 18, name: 'J5 电机总线分支', role: '腕俯仰供电' },
|
|||
|
|
{ idx: 19, name: 'J6 电机总线分支', role: '末端供电' }
|
|||
|
|
],
|
|||
|
|
screws: [
|
|||
|
|
{ spec: '2.5×100mm 阻燃扎带', qty: 12, role: '线束固定锚点锁紧' }
|
|||
|
|
],
|
|||
|
|
tips: '慢速全行程转动各关节,目视确认线束弯曲半径均大于 15mm 且无边缘摩擦。',
|
|||
|
|
sheetRef: 0,
|
|||
|
|
camera: { az: -0.7, po: 1.15, rd: 580, tx: 0, ty: 180, tz: 380 }
|
|||
|
|
}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
function applySheetTransform() {
|
|||
|
|
const canvas = document.getElementById('sheetCanvas');
|
|||
|
|
if (canvas) {
|
|||
|
|
canvas.style.transform = `translate(${sheetPanX}px, ${sheetPanY}px) scale(${sheetScale})`;
|
|||
|
|
}
|
|||
|
|
const badge = document.getElementById('sheetZoomBadge');
|
|||
|
|
if (badge) {
|
|||
|
|
badge.textContent = Math.round(sheetScale * 100) + '%';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function fitSheetToScreen() {
|
|||
|
|
const svg = document.querySelector('#sheetSvgWrapper svg');
|
|||
|
|
if (!svg) return;
|
|||
|
|
const vb = svg.viewBox.baseVal;
|
|||
|
|
const W = (vb && vb.width) ? vb.width : (svg.clientWidth || 1200);
|
|||
|
|
const H = (vb && vb.height) ? vb.height : (svg.clientHeight || 800);
|
|||
|
|
const vp = document.getElementById('sheetViewport');
|
|||
|
|
if (!vp) return;
|
|||
|
|
const vw = vp.clientWidth, vh = vp.clientHeight;
|
|||
|
|
if (!vw || !vh) return;
|
|||
|
|
|
|||
|
|
const pad = 36;
|
|||
|
|
sheetScale = Math.min((vw - pad * 2) / W, (vh - pad * 2) / H);
|
|||
|
|
sheetScale = Math.max(0.15, Math.min(sheetScale, 3.0));
|
|||
|
|
sheetPanX = (vw - W * sheetScale) / 2;
|
|||
|
|
sheetPanY = (vh - H * sheetScale) / 2;
|
|||
|
|
applySheetTransform();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderSheet(idx) {
|
|||
|
|
curSheetIdx = idx;
|
|||
|
|
const isGuide = idx === 'guide';
|
|||
|
|
const vp = document.getElementById('sheetViewport');
|
|||
|
|
const gv = document.getElementById('sheetGuideView');
|
|||
|
|
|
|||
|
|
// Update tabs
|
|||
|
|
document.querySelectorAll('#sheetTabs .sheet-tab').forEach(b => {
|
|||
|
|
b.classList.toggle('on', b.dataset.sheet === String(idx));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (isGuide) {
|
|||
|
|
if (vp) vp.style.display = 'none';
|
|||
|
|
if (gv) gv.style.display = 'flex';
|
|||
|
|
renderGuideStep(currentGuideStep);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (gv) gv.style.display = 'none';
|
|||
|
|
if (vp) vp.style.display = 'block';
|
|||
|
|
|
|||
|
|
const wrapper = document.getElementById('sheetSvgWrapper');
|
|||
|
|
if (wrapper && typeof SHEETS !== 'undefined' && SHEETS[idx]) {
|
|||
|
|
wrapper.innerHTML = SHEETS[idx];
|
|||
|
|
const svg = wrapper.querySelector('svg');
|
|||
|
|
if (svg) {
|
|||
|
|
const vb = svg.viewBox && svg.viewBox.baseVal;
|
|||
|
|
const w = (vb && vb.width) || 1200;
|
|||
|
|
const h = (vb && vb.height) || 800;
|
|||
|
|
svg.setAttribute('width', w);
|
|||
|
|
svg.setAttribute('height', h);
|
|||
|
|
svg.style.width = w + 'px';
|
|||
|
|
svg.style.height = h + 'px';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update sheet title & desc
|
|||
|
|
const meta = SHEET_METAS[idx] || {};
|
|||
|
|
const tEl = document.getElementById('sheetInfoTitle');
|
|||
|
|
const dEl = document.getElementById('sheetInfoDesc');
|
|||
|
|
if (tEl) tEl.innerHTML = `<b>${meta.title || ''}</b>`;
|
|||
|
|
if (dEl) dEl.textContent = meta.desc || '';
|
|||
|
|
|
|||
|
|
// Apply layer toggles
|
|||
|
|
applySheetLayers();
|
|||
|
|
|
|||
|
|
// Reset highlight & tooltip
|
|||
|
|
pinnedPartIdx = null;
|
|||
|
|
clearSheetPartHighlight();
|
|||
|
|
|
|||
|
|
// Fit to screen
|
|||
|
|
setTimeout(fitSheetToScreen, 20);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function applySheetLayers() {
|
|||
|
|
const svg = document.querySelector('#sheetSvgWrapper svg');
|
|||
|
|
if (!svg) return;
|
|||
|
|
const bBal = document.getElementById('chkLyrBalloons');
|
|||
|
|
const bDim = document.getElementById('chkLyrDims');
|
|||
|
|
const bAxe = document.getElementById('chkLyrAxes');
|
|||
|
|
const bLeg = document.getElementById('chkLyrLegend');
|
|||
|
|
|
|||
|
|
const showBal = bBal ? bBal.checked : true;
|
|||
|
|
const showDim = bDim ? bDim.checked : true;
|
|||
|
|
const showAxe = bAxe ? bAxe.checked : true;
|
|||
|
|
const showLeg = bLeg ? bLeg.checked : true;
|
|||
|
|
|
|||
|
|
svg.querySelectorAll('.sheet-balloons-group, .sheet-balloon, .sheet-leader').forEach(el => {
|
|||
|
|
el.style.display = showBal ? '' : 'none';
|
|||
|
|
});
|
|||
|
|
svg.querySelectorAll('.sheet-dims-group, .sheet-dim, .sheet-dim-line, .sheet-dim-ext, .sheet-dim-text').forEach(el => {
|
|||
|
|
el.style.display = showDim ? '' : 'none';
|
|||
|
|
});
|
|||
|
|
svg.querySelectorAll('.sheet-axes-group, .sheet-axis, .sheet-axis-label').forEach(el => {
|
|||
|
|
el.style.display = showAxe ? '' : 'none';
|
|||
|
|
});
|
|||
|
|
svg.querySelectorAll('.sheet-legend-box, .sheet-legend-row').forEach(el => {
|
|||
|
|
el.style.display = showLeg ? '' : 'none';
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function highlightSheetPart(idx, mouseEvent) {
|
|||
|
|
const svg = document.querySelector('#sheetSvgWrapper svg');
|
|||
|
|
if (!svg) return;
|
|||
|
|
|
|||
|
|
// Remove previous hover classes
|
|||
|
|
svg.querySelectorAll('.is-hovered').forEach(el => el.classList.remove('is-hovered'));
|
|||
|
|
|
|||
|
|
// Highlight all elements with data-idx=idx
|
|||
|
|
const targets = svg.querySelectorAll(`[data-idx="${idx}"]`);
|
|||
|
|
targets.forEach(el => el.classList.add('is-hovered'));
|
|||
|
|
|
|||
|
|
// Highlight row in Left Tree / BOM
|
|||
|
|
highlightLeftTreeRow(idx);
|
|||
|
|
|
|||
|
|
// Show Tooltip HUD
|
|||
|
|
showSheetTooltip(idx, mouseEvent);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function clearSheetPartHighlight() {
|
|||
|
|
const svg = document.querySelector('#sheetSvgWrapper svg');
|
|||
|
|
if (svg) {
|
|||
|
|
svg.querySelectorAll('.is-hovered').forEach(el => el.classList.remove('is-hovered'));
|
|||
|
|
}
|
|||
|
|
const tt = document.getElementById('sheetTooltip');
|
|||
|
|
if (tt && !pinnedPartIdx) tt.style.display = 'none';
|
|||
|
|
clearLeftTreeRow();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showSheetTooltip(idx, e) {
|
|||
|
|
const tt = document.getElementById('sheetTooltip');
|
|||
|
|
if (!tt) return;
|
|||
|
|
|
|||
|
|
const part = DATA.parts.find(p => p.idx === idx);
|
|||
|
|
if (!part) {
|
|||
|
|
tt.style.display = 'none';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const isPrinted = part.printed;
|
|||
|
|
const volCm3 = (part.vol / 1000).toFixed(1);
|
|||
|
|
const wtG = Math.round((part.vol / 1000) * 1.25);
|
|||
|
|
const dimsStr = part.dims ? `${Math.round(part.dims[0])} × ${Math.round(part.dims[1])} × ${Math.round(part.dims[2])} mm` : '-';
|
|||
|
|
const screwText = part.name.startsWith('P') ? 'M2/M3 内六角紧固螺钉' : '输出法兰配合';
|
|||
|
|
|
|||
|
|
tt.innerHTML = `
|
|||
|
|
<div class="st-title">
|
|||
|
|
<span>#${idx} ${part.cn || part.name}</span>
|
|||
|
|
<span class="st-tag ${isPrinted ? 'pr' : 'ex'}">${isPrinted ? 'PLA 打印件' : '外购模组'}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="st-row"><span>设计编号:</span><span>${part.name.split('_')[0] || part.name}</span></div>
|
|||
|
|
<div class="st-row"><span>包络三围:</span><span>${dimsStr}</span></div>
|
|||
|
|
<div class="st-row"><span>净重估算:</span><span>${wtG} g (${volCm3} cm³)</span></div>
|
|||
|
|
<div class="st-row"><span>连接关系:</span><span>${screwText}</span></div>
|
|||
|
|
<button class="st-btn" onclick="jumpToPart3D(${idx})">🧊 在 3D 模型中定位高亮</button>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
tt.style.display = 'block';
|
|||
|
|
|
|||
|
|
// Position tooltip
|
|||
|
|
const vp = document.getElementById('sheetViewport');
|
|||
|
|
if (e && vp) {
|
|||
|
|
const vr = vp.getBoundingClientRect();
|
|||
|
|
let x = e.clientX - vr.left + 16;
|
|||
|
|
let y = e.clientY - vr.top + 16;
|
|||
|
|
if (x + 310 > vr.width) x = e.clientX - vr.left - 320;
|
|||
|
|
if (y + 180 > vr.height) y = e.clientY - vr.top - 180;
|
|||
|
|
tt.style.left = Math.max(10, x) + 'px';
|
|||
|
|
tt.style.top = Math.max(10, y) + 'px';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function highlightLeftTreeRow(idx) {
|
|||
|
|
document.querySelectorAll('#tree .row').forEach(r => {
|
|||
|
|
if (r.dataset && r.dataset.idx === String(idx)) {
|
|||
|
|
r.style.background = '#1e3a66';
|
|||
|
|
r.style.borderRadius = '3px';
|
|||
|
|
} else {
|
|||
|
|
r.style.background = '';
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function clearLeftTreeRow() {
|
|||
|
|
document.querySelectorAll('#tree .row').forEach(r => r.style.background = '');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function jumpToPart3D(idx) {
|
|||
|
|
const tabModel = document.querySelector('.tab[data-view="model"]');
|
|||
|
|
if (tabModel) tabModel.click();
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
const m = meshes.find(x => x.userData && x.userData.idx === idx);
|
|||
|
|
if (!m) return;
|
|||
|
|
if (sel && !sel.userData.screw && !sel.userData.harness) {
|
|||
|
|
sel.material.emissive.setHex(0);
|
|||
|
|
sel.material.emissiveIntensity = 0;
|
|||
|
|
}
|
|||
|
|
sel = m;
|
|||
|
|
m.visible = true;
|
|||
|
|
if (m.chk) m.chk.checked = true;
|
|||
|
|
m.material.emissive.setHex(0x2f6bff);
|
|||
|
|
m.material.emissiveIntensity = 0.8;
|
|||
|
|
|
|||
|
|
const box = new THREE.Box3().setFromObject(m);
|
|||
|
|
const center = new THREE.Vector3();
|
|||
|
|
box.getCenter(center);
|
|||
|
|
tx = center.x; ty = center.y; tz = center.z;
|
|||
|
|
rd = Math.max(220, box.getSize(new THREE.Vector3()).length() * 2.2);
|
|||
|
|
requestRender();
|
|||
|
|
|
|||
|
|
const hud = document.getElementById('hud');
|
|||
|
|
if (hud) {
|
|||
|
|
hud.innerHTML = `<b>已在3D中定位: #${idx} ${m.userData.cn || ''}</b> · ` +
|
|||
|
|
(m.userData.printed ? 'PLA打印件' : '外购电机/标准件') +
|
|||
|
|
` <span class="hudlink" onclick="document.querySelector('.tab[data-view=sheet]').click()">[返回装配图]</span>`;
|
|||
|
|
}
|
|||
|
|
}, 60);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderGuideStep(stepIdx) {
|
|||
|
|
currentGuideStep = Math.max(0, Math.min(stepIdx, ASSEMBLY_STEPS.length - 1));
|
|||
|
|
const step = ASSEMBLY_STEPS[currentGuideStep];
|
|||
|
|
if (!step) return;
|
|||
|
|
|
|||
|
|
// Render step chips
|
|||
|
|
const bar = document.getElementById('guideStepsBar');
|
|||
|
|
if (bar) {
|
|||
|
|
bar.innerHTML = ASSEMBLY_STEPS.map((s, i) => `
|
|||
|
|
<div class="guide-step-chip ${i === currentGuideStep ? 'active' : ''}" onclick="renderGuideStep(${i})">
|
|||
|
|
<b>步骤 ${s.step}</b> ${s.title.split(' ')[0]}
|
|||
|
|
</div>
|
|||
|
|
`).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Render left panel
|
|||
|
|
const pLeft = document.getElementById('guidePanelLeft');
|
|||
|
|
if (pLeft) {
|
|||
|
|
pLeft.innerHTML = `
|
|||
|
|
<div style="font-size:11px;color:var(--warn);font-weight:bold;letter-spacing:.6px">工序 ${step.step} / ${ASSEMBLY_STEPS.length}</div>
|
|||
|
|
<div style="font-size:15px;font-weight:bold;color:#fff">${step.title}</div>
|
|||
|
|
<div style="font-size:12px;color:var(--dim);margin-top:-6px">${step.subtitle}</div>
|
|||
|
|
|
|||
|
|
<div class="guide-card">
|
|||
|
|
<div class="guide-card-title">📖 装配任务说明</div>
|
|||
|
|
<div style="font-size:12px;line-height:1.6;color:var(--tx)">${step.desc}</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="guide-card">
|
|||
|
|
<div class="guide-card-title">📦 本工序构件 (${step.parts.length} 件)</div>
|
|||
|
|
<div class="guide-part-list">
|
|||
|
|
${step.parts.map(p => `
|
|||
|
|
<div class="guide-part-badge" onclick="jumpToPart3D(${p.idx})">
|
|||
|
|
<b>#${p.idx}</b> ${p.name} <span style="color:var(--dim);font-size:10px">(${p.role})</span>
|
|||
|
|
</div>
|
|||
|
|
`).join('')}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="guide-card">
|
|||
|
|
<div class="guide-card-title">🔩 紧固件与螺钉清单</div>
|
|||
|
|
<div style="display:flex;flex-direction:column;gap:5px;font-size:11.5px">
|
|||
|
|
${step.screws.map(s => `
|
|||
|
|
<div style="display:flex;justify-content:space-between;color:var(--tx)">
|
|||
|
|
<span><b>${s.spec}</b> × ${s.qty}</span>
|
|||
|
|
<span style="color:var(--dim)">${s.role}</span>
|
|||
|
|
</div>
|
|||
|
|
`).join('')}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="guide-card" style="border-color:rgba(245,158,11,.4);background:rgba(245,158,11,.06)">
|
|||
|
|
<div class="guide-card-title">⚠️ 工艺公差与装配要点</div>
|
|||
|
|
<div style="font-size:11.5px;color:#fef3c7;line-height:1.5">${step.tips}</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="guide-actions">
|
|||
|
|
<button class="guide-btn guide-btn-nav" ${currentGuideStep === 0 ? 'disabled style="opacity:.4"' : ''} onclick="renderGuideStep(${currentGuideStep - 1})">⬅ 上一步</button>
|
|||
|
|
<button class="guide-btn guide-btn-nav" ${currentGuideStep === ASSEMBLY_STEPS.length - 1 ? 'disabled style="opacity:.4"' : ''} onclick="renderGuideStep(${currentGuideStep + 1})">下一步 ➡</button>
|
|||
|
|
<button class="guide-btn guide-btn-3d" onclick="showAssemblyStepIn3D(${currentGuideStep})">🧊 3D模拟装配</button>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Render right preview: show relevant sheet for this step
|
|||
|
|
const cw = document.getElementById('guideCanvasWrapper');
|
|||
|
|
if (cw && typeof SHEETS !== 'undefined') {
|
|||
|
|
cw.innerHTML = SHEETS[step.sheetRef];
|
|||
|
|
const svg = cw.querySelector('svg');
|
|||
|
|
if (svg) {
|
|||
|
|
svg.style.maxWidth = '92%';
|
|||
|
|
svg.style.maxHeight = '92%';
|
|||
|
|
svg.style.width = 'auto';
|
|||
|
|
svg.style.height = 'auto';
|
|||
|
|
svg.style.boxShadow = '0 10px 40px rgba(0,0,0,.6)';
|
|||
|
|
svg.style.borderRadius = '6px';
|
|||
|
|
|
|||
|
|
// Highlight step parts in SVG
|
|||
|
|
const stepPartIdxs = step.parts.map(p => p.idx);
|
|||
|
|
svg.querySelectorAll('.sheet-part-path').forEach(p => {
|
|||
|
|
const idx = parseInt(p.dataset.idx);
|
|||
|
|
if (stepPartIdxs.includes(idx)) {
|
|||
|
|
p.style.stroke = '#f59e0b';
|
|||
|
|
p.style.strokeWidth = '2.8px';
|
|||
|
|
p.style.filter = 'drop-shadow(0 0 6px #f59e0b)';
|
|||
|
|
} else {
|
|||
|
|
p.style.opacity = '0.35';
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
svg.querySelectorAll('.sheet-balloon').forEach(b => {
|
|||
|
|
const idx = parseInt(b.dataset.idx);
|
|||
|
|
if (stepPartIdxs.includes(idx)) {
|
|||
|
|
const c = b.querySelector('.balloon-circle');
|
|||
|
|
if (c) { c.style.fill = '#f59e0b'; c.style.stroke = '#ffffff'; }
|
|||
|
|
const t = b.querySelector('.balloon-text');
|
|||
|
|
if (t) t.style.fill = '#ffffff';
|
|||
|
|
} else {
|
|||
|
|
b.style.opacity = '0.3';
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showAssemblyStepIn3D(stepIdx) {
|
|||
|
|
const step = ASSEMBLY_STEPS[stepIdx];
|
|||
|
|
if (!step) return;
|
|||
|
|
|
|||
|
|
const tabModel = document.querySelector('.tab[data-view="model"]');
|
|||
|
|
if (tabModel) tabModel.click();
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (step.camera) {
|
|||
|
|
az = step.camera.az; po = step.camera.po; rd = step.camera.rd;
|
|||
|
|
tx = step.camera.tx; ty = step.camera.ty; tz = step.camera.tz;
|
|||
|
|
}
|
|||
|
|
const stepPartIdxs = step.parts.map(p => p.idx);
|
|||
|
|
meshes.forEach(m => {
|
|||
|
|
if (m.userData && stepPartIdxs.includes(m.userData.idx)) {
|
|||
|
|
m.visible = true;
|
|||
|
|
if (m.chk) m.chk.checked = true;
|
|||
|
|
m.material.emissive.setHex(0xd97706);
|
|||
|
|
m.material.emissiveIntensity = 0.65;
|
|||
|
|
} else if (m.userData && !m.userData.screw && !m.userData.harness) {
|
|||
|
|
m.material.emissive.setHex(0);
|
|||
|
|
m.material.emissiveIntensity = 0;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
requestRender();
|
|||
|
|
|
|||
|
|
const hud = document.getElementById('hud');
|
|||
|
|
if (hud) {
|
|||
|
|
hud.innerHTML = `<b>装配向导第 ${step.step} 步: ${step.title}</b> · 构件高亮呈现 ` +
|
|||
|
|
`<span class="hudlink" onclick="document.querySelector('.tab[data-view=sheet]').click()">[返回装配向导]</span>`;
|
|||
|
|
}
|
|||
|
|
}, 60);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function initSheetViewer() {
|
|||
|
|
if (sheetInited) return;
|
|||
|
|
sheetInited = true;
|
|||
|
|
|
|||
|
|
const vp = document.getElementById('sheetViewport');
|
|||
|
|
const canvas = document.getElementById('sheetCanvas');
|
|||
|
|
|
|||
|
|
// Sheet Tabs Click
|
|||
|
|
document.querySelectorAll('#sheetTabs .sheet-tab').forEach(b => {
|
|||
|
|
b.onclick = () => {
|
|||
|
|
const idx = b.dataset.sheet === 'guide' ? 'guide' : parseInt(b.dataset.sheet);
|
|||
|
|
renderSheet(idx);
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Exit Guide button
|
|||
|
|
const btnExit = document.getElementById('btnExitGuide');
|
|||
|
|
if (btnExit) btnExit.onclick = () => renderSheet(0);
|
|||
|
|
|
|||
|
|
// Theme Switcher Click
|
|||
|
|
document.querySelectorAll('#sheetToolbar [data-theme]').forEach(b => {
|
|||
|
|
b.onclick = () => {
|
|||
|
|
document.querySelectorAll('#sheetToolbar [data-theme]').forEach(x => x.classList.remove('on'));
|
|||
|
|
b.classList.add('on');
|
|||
|
|
curSheetTheme = b.dataset.theme;
|
|||
|
|
const sh = document.getElementById('sheet');
|
|||
|
|
if (sh) sh.className = 'theme-' + curSheetTheme;
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Layer Checkboxes
|
|||
|
|
['chkLyrBalloons', 'chkLyrDims', 'chkLyrAxes', 'chkLyrLegend'].forEach(id => {
|
|||
|
|
const el = document.getElementById(id);
|
|||
|
|
if (el) el.onchange = () => applySheetLayers();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 3D Bridge Button
|
|||
|
|
const btn3D = document.getElementById('btnSheetTo3D');
|
|||
|
|
if (btn3D) {
|
|||
|
|
btn3D.onclick = () => {
|
|||
|
|
if (pinnedPartIdx) jumpToPart3D(pinnedPartIdx);
|
|||
|
|
else {
|
|||
|
|
const tabModel = document.querySelector('.tab[data-view="model"]');
|
|||
|
|
if (tabModel) tabModel.click();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Float Bar Buttons
|
|||
|
|
const sfbIn = document.getElementById('sfbIn');
|
|||
|
|
if (sfbIn) sfbIn.onclick = () => {
|
|||
|
|
sheetScale = Math.min(6.0, sheetScale * 1.25);
|
|||
|
|
applySheetTransform();
|
|||
|
|
};
|
|||
|
|
const sfbOut = document.getElementById('sfbOut');
|
|||
|
|
if (sfbOut) sfbOut.onclick = () => {
|
|||
|
|
sheetScale = Math.max(0.15, sheetScale / 1.25);
|
|||
|
|
applySheetTransform();
|
|||
|
|
};
|
|||
|
|
const sfbFit = document.getElementById('sfbFit');
|
|||
|
|
if (sfbFit) sfbFit.onclick = () => fitSheetToScreen();
|
|||
|
|
const sfb100 = document.getElementById('sfb100');
|
|||
|
|
if (sfb100) sfb100.onclick = () => {
|
|||
|
|
sheetScale = 1.0;
|
|||
|
|
const vp = document.getElementById('sheetViewport');
|
|||
|
|
const svg = document.querySelector('#sheetSvgWrapper svg');
|
|||
|
|
if (vp && svg) {
|
|||
|
|
sheetPanX = (vp.clientWidth - (svg.viewBox.baseVal.width || 1200)) / 2;
|
|||
|
|
sheetPanY = (vp.clientHeight - (svg.viewBox.baseVal.height || 800)) / 2;
|
|||
|
|
}
|
|||
|
|
applySheetTransform();
|
|||
|
|
};
|
|||
|
|
const sfbReset = document.getElementById('sfbReset');
|
|||
|
|
if (sfbReset) sfbReset.onclick = () => fitSheetToScreen();
|
|||
|
|
const sfbFull = document.getElementById('sfbFull');
|
|||
|
|
if (sfbFull) {
|
|||
|
|
sfbFull.onclick = () => {
|
|||
|
|
const sh = document.getElementById('sheet');
|
|||
|
|
if (!document.fullscreenElement) {
|
|||
|
|
if (sh && sh.requestFullscreen) sh.requestFullscreen();
|
|||
|
|
} else {
|
|||
|
|
if (document.exitFullscreen) document.exitFullscreen();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const badge = document.getElementById('sheetZoomBadge');
|
|||
|
|
if (badge) badge.onclick = () => fitSheetToScreen();
|
|||
|
|
|
|||
|
|
// Viewport Wheel Zoom
|
|||
|
|
if (vp) {
|
|||
|
|
vp.addEventListener('wheel', e => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
const rect = vp.getBoundingClientRect();
|
|||
|
|
const mx = e.clientX - rect.left;
|
|||
|
|
const my = e.clientY - rect.top;
|
|||
|
|
const factor = e.deltaY < 0 ? 1.15 : 0.87;
|
|||
|
|
const newScale = Math.max(0.15, Math.min(6.0, sheetScale * factor));
|
|||
|
|
sheetPanX = mx - (mx - sheetPanX) * (newScale / sheetScale);
|
|||
|
|
sheetPanY = my - (my - sheetPanY) * (newScale / sheetScale);
|
|||
|
|
sheetScale = newScale;
|
|||
|
|
applySheetTransform();
|
|||
|
|
}, { passive: false });
|
|||
|
|
|
|||
|
|
// Drag Pan
|
|||
|
|
vp.addEventListener('mousedown', e => {
|
|||
|
|
if (e.button !== 0 && e.button !== 1) return;
|
|||
|
|
isSheetDragging = true;
|
|||
|
|
vp.classList.add('is-dragging');
|
|||
|
|
sheetDragStartX = e.clientX - sheetPanX;
|
|||
|
|
sheetDragStartY = e.clientY - sheetPanY;
|
|||
|
|
});
|
|||
|
|
window.addEventListener('mousemove', e => {
|
|||
|
|
if (!isSheetDragging) return;
|
|||
|
|
sheetPanX = e.clientX - sheetDragStartX;
|
|||
|
|
sheetPanY = e.clientY - sheetDragStartY;
|
|||
|
|
applySheetTransform();
|
|||
|
|
});
|
|||
|
|
window.addEventListener('mouseup', () => {
|
|||
|
|
if (isSheetDragging) {
|
|||
|
|
isSheetDragging = false;
|
|||
|
|
vp.classList.remove('is-dragging');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
vp.addEventListener('dblclick', e => {
|
|||
|
|
if (e.target.closest('.sheet-part-path') || e.target.closest('.sheet-balloon')) return;
|
|||
|
|
fitSheetToScreen();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Interactive Hover & Click on Canvas
|
|||
|
|
if (canvas) {
|
|||
|
|
canvas.addEventListener('mouseover', e => {
|
|||
|
|
const hit = e.target.closest('[data-idx]');
|
|||
|
|
if (!hit) return;
|
|||
|
|
const idx = parseInt(hit.dataset.idx);
|
|||
|
|
if (!isNaN(idx)) highlightSheetPart(idx, e);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
canvas.addEventListener('mouseout', e => {
|
|||
|
|
const hit = e.target.closest('[data-idx]');
|
|||
|
|
if (!hit) return;
|
|||
|
|
if (!pinnedPartIdx) clearSheetPartHighlight();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
canvas.addEventListener('click', e => {
|
|||
|
|
const hit = e.target.closest('[data-idx]');
|
|||
|
|
if (hit) {
|
|||
|
|
const idx = parseInt(hit.dataset.idx);
|
|||
|
|
if (!isNaN(idx)) {
|
|||
|
|
if (pinnedPartIdx === idx) {
|
|||
|
|
pinnedPartIdx = null;
|
|||
|
|
clearSheetPartHighlight();
|
|||
|
|
} else {
|
|||
|
|
pinnedPartIdx = idx;
|
|||
|
|
highlightSheetPart(idx, e);
|
|||
|
|
const tt = document.getElementById('sheetTooltip');
|
|||
|
|
if (tt) tt.classList.add('pinned');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
pinnedPartIdx = null;
|
|||
|
|
clearSheetPartHighlight();
|
|||
|
|
const tt = document.getElementById('sheetTooltip');
|
|||
|
|
if (tt) { tt.classList.remove('pinned'); tt.style.display = 'none'; }
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Initial render of first sheet
|
|||
|
|
renderSheet(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openSheetView() {
|
|||
|
|
initSheetViewer();
|
|||
|
|
const footM = document.getElementById('footModel');
|
|||
|
|
const footS = document.getElementById('footSheet');
|
|||
|
|
if (footM) footM.style.display = 'none';
|
|||
|
|
if (footS) footS.style.display = 'inline';
|
|||
|
|
setTimeout(fitSheetToScreen, 40);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.querySelectorAll('.tab[data-view]').forEach(b => b.onclick = () => {
|
|||
|
|
document.querySelectorAll('.tab[data-view]').forEach(x => x.classList.remove('on'));
|
|||
|
|
b.classList.add('on');
|
|||
|
|
const s = b.dataset.view === 'sheet';
|
|||
|
|
document.body.classList.toggle('sheet-mode', s);
|
|||
|
|
sheet.style.display = s ? 'flex' : 'none';
|
|||
|
|
cv.style.display = s ? 'none' : 'block';
|
|||
|
|
const footM = document.getElementById('footModel');
|
|||
|
|
const footS = document.getElementById('footSheet');
|
|||
|
|
if (s) {
|
|||
|
|
if (footM) footM.style.display = 'none';
|
|||
|
|
if (footS) footS.style.display = 'inline';
|
|||
|
|
openSheetView();
|
|||
|
|
} else {
|
|||
|
|
if (footM) footM.style.display = 'inline';
|
|||
|
|
if (footS) footS.style.display = 'none';
|
|||
|
|
resize();
|
|||
|
|
requestRender();
|
|||
|
|
}
|
|||
|
|
});
|