TEM 标尺重绘 EDS 叠加 文献插入 去AI化 U-Net 原子像去噪 衍射匹配 多晶衍射环
1 选择图片
2 设置中心
3 设置标尺
4 设置晶体
5 计算标定
→ 在右侧上传衍射图 / 原子像
💡 Ctrl + 滚轮 缩放图像
`; } function exportHtmlReport(){ const html = buildReportHtml(cv.toDataURL('image/png'), STATE.matchResult, STATE.phases); downloadBlob(html, 'ring_report.html', 'text/html'); } // 导出 JSON:结构化分析数据,方便用户脚本进一步处理 function exportJson(){ const mr = STATE.matchResult; const payload = { schema: 'poly-ring-analysis-v1', timestamp: new Date().toISOString(), image: { shape: STATE.shape, fft_mode: !!STATE.fftMode, }, center: STATE.center, calibration: { calib_recip_nm_per_px: mr.calibration, scalebar_px: STATE.scalebarPx, scalebar_real: STATE.scalebarReal, scalebar_unit: STATE.scalebarUnit, }, phases: STATE.phases.map(p => ({ name: p.name, color: p.color, lattice: p.lattice, cell: p.cell, hexagonal: !!p.hexagonal, dlist_count: p.dlist.length, })), detected_rings: mr.detected.map(det => ({ r_px: det.r_px, d_exp_A: det.d_exp, manual: !!det.manual, matches: det.matches.map(m => ({ phase: m.phase, hkl: m.hkl, d_theory_A: m.d_theory, err_pct: m.err_pct, I_rel: m.I_rel, })), })), theory_rings: mr.theory_rings.map((t, i) => ({ phase: t.phase, hkl: t.hkl, d_A: t.d, r_px: t.r_px, I_rel: t.I_rel, allowed: t.allowed, color: t.color, hidden: STATE.uiHidden.has(i), // D6: read from Set, not mutated server object })), }; downloadBlob(JSON.stringify(payload, null, 2), 'ring_analysis.json', 'application/json'); } // ── Canvas 渲染引擎 ── const cv = document.getElementById('ringCanvas'); const ctx = cv.getContext('2d'); let VIEW = {scale: 1}; // 图像像素 → canvas 像素 // wrap 永远是正方形:边长 = 52vh × viewZoom,cap 列宽 / viewport-240 function applyZoom(){ const wrap = cv.parentElement; const baseSquare = window.innerHeight * 0.52; const z = STATE.viewZoom || 1.0; const colW = wrap.parentElement.clientWidth; let size = baseSquare * z; size = Math.min(size, colW, window.innerHeight - 240); size = Math.max(size, 180); wrap.style.width = size + 'px'; wrap.style.height = size + 'px'; } function resizeCanvas(){ applyZoom(); if (!STATE.shape) return; const aspect = STATE.shape[0] / STATE.shape[1]; // h/w const sq = cv.parentElement.clientWidth; // wrap 是方的 let w, h; if (aspect >= 1){ h = sq; w = sq / aspect; } // 高瘦/方 → h 撑满 else { w = sq; h = sq * aspect; } // 矮胖 → w 撑满 cv.width = w; cv.height = h; cv.style.width = w + 'px'; cv.style.height = h + 'px'; VIEW.scale = cv.width / STATE.shape[1]; const hint = document.getElementById('canvasEmptyHint'); if (hint) hint.style.display = 'none'; } // Ctrl+滚轮 = 实时缩放预览框;不带 Ctrl 让 step-2 handler 处理(辅助圆) cv.parentElement.addEventListener('wheel', (e) => { if (!e.ctrlKey) return; e.preventDefault(); const factor = e.deltaY < 0 ? 1.1 : 0.9; STATE.viewZoom = Math.max(0.3, Math.min(3.0, (STATE.viewZoom || 1.0) * factor)); if (STATE.shape){ resizeCanvas(); draw(); } else { applyZoom(); } }, { passive:false }); // 页面初始化也要应用一次,让空状态就是方框 applyZoom(); window.addEventListener('resize', () => { if (STATE.shape){ resizeCanvas(); draw(); } else { applyZoom(); } }); const img2cv = (x, y) => [x * VIEW.scale, y * VIEW.scale]; const cv2img = (x, y) => [x / VIEW.scale, y / VIEW.scale]; function draw(){ if (!STATE.img.src) return; ctx.clearRect(0, 0, cv.width, cv.height); ctx.drawImage(STATE.img, 0, 0, cv.width, cv.height); // 中心标记 if (STATE.center && document.getElementById('lyrCenter').checked){ const [cx, cy] = img2cv(STATE.center.cx, STATE.center.cy); ctx.strokeStyle = STATE.center.confidence < 0.3 ? '#ff5555' : '#00e5ff'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(cx-12,cy); ctx.lineTo(cx+12,cy); ctx.moveTo(cx,cy-12); ctx.lineTo(cx,cy+12); ctx.stroke(); // 步骤 2:辅助圆 if (STATE.step === 2 && STATE._auxR){ ctx.beginPath(); ctx.arc(cx, cy, STATE._auxR * VIEW.scale, 0, 7); ctx.stroke(); } } // 步骤 3:标尺线 if (STATE.step === 3 && STATE._scalebarLine){ const [x1,y1] = img2cv(...STATE._scalebarLine[0]); const [x2,y2] = img2cv(...STATE._scalebarLine[1]); ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke(); } // 步骤 3:Ctrl+click 识别到的标尺 bbox(黄色高亮) if (STATE.step === 3 && STATE._scalebarBbox){ const [bx0, by0] = img2cv(STATE._scalebarBbox[0], STATE._scalebarBbox[1]); const [bx1, by1] = img2cv(STATE._scalebarBbox[2], STATE._scalebarBbox[3]); ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 2; ctx.strokeRect(bx0 - 2, by0 - 4, bx1 - bx0 + 4, Math.max(8, by1 - by0 + 8)); ctx.fillStyle = '#ffd700'; ctx.font = 'bold 13px sans-serif'; ctx.fillText(STATE.scalebarPx.toFixed(0) + ' px', bx0, by0 - 8); } // 步骤 5:理论环 + 斑点 if (STATE.step === 5 && STATE.matchResult && document.getElementById('lyrRings').checked){ drawTheoryRings(); } } // 理论环 JS 实时绘制(纯几何,无需 server) function phaseIsHex(phaseName){ const p = STATE.phases.find(x => x.name === phaseName); return !!(p && p.hexagonal); } function drawTheoryRings(){ if (!STATE.center) return; const [ccx, ccy] = img2cv(STATE.center.cx, STATE.center.cy); STATE.matchResult.theory_rings.forEach((t, i) => { if (STATE.uiHidden.has(i)) return; // D6: uiHidden Set if (!t.allowed && !STATE.showExtinct) return; const r = t.r_px * VIEW.scale; ctx.strokeStyle = t.color; ctx.globalAlpha = t.allowed ? 0.9 : 0.35; ctx.lineWidth = 1; ctx.setLineDash(t.allowed ? [] : [4,4]); ctx.beginPath(); ctx.arc(ccx, ccy, r, 0, 7); ctx.stroke(); ctx.setLineDash([]); // hkl 标签(六方相且 showHex 开 → 4 指数) ctx.fillStyle = t.color; ctx.font = '10px monospace'; ctx.fillText(fmtHKL(t.hkl, phaseIsHex(t.phase)), ccx + r * 0.7, ccy - r * 0.7); }); ctx.globalAlpha = 1; } // hkl → 字符串。负数用 -N(不用 macron 上划线,跨字体渲染不稳)。 // hexagonal 且 STATE.showHex 为真 → 转 4 指数 hkil,i = -(h+k)。 function fmtHKL(hkl, hexagonal){ let idx = hkl.slice(); if (hexagonal && STATE.showHex){ const i = -(hkl[0] + hkl[1]); idx = [hkl[0], hkl[1], i, hkl[2]]; } return '(' + idx.map(n => n < 0 ? ('-' + (-n)) : ('' + n)).join(' ') + ')'; } // ── 步骤 2:设中心 ── function step2HTML(){ return `
设置透射斑中心

点击/拖动 canvas 设中心;方向键微调;滚轮调辅助圆。对称斑都落在辅助圆上 = 中心准。

`; } // "还原自动检测值":恢复 load 时存的 STATE.centerGuess0(不重新跑检测—— // 图未变,重跑结果相同;FFT 模式下 centerGuess0 为 null,此按钮应隐藏/禁用)。 function resetCenterToGuess(){ if (STATE.centerGuess0){ STATE.center = JSON.parse(JSON.stringify(STATE.centerGuess0)); renderPanel(); draw(); } } // ── 初始化 ── window.addEventListener('resize', () => { if (STATE.img.src) { resizeCanvas(); draw(); } }); renderPanel();