119 lines
9.4 KiB
JavaScript
119 lines
9.4 KiB
JavaScript
// part.js — part detail. Left: a tabbed visualization (real 3D model / schematic symbol / footprint),
|
||
// all built from the curated data. Right: the full record (specs, pins, SPICE, downloads).
|
||
import { mountViewer } from '/assets/viewer.js';
|
||
const esc = (s) => (s == null ? '' : String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])));
|
||
const id = new URLSearchParams(location.search).get('id');
|
||
|
||
function dl(name, text, mime = 'text/plain') { const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([text], { type: mime })); a.download = name; a.click(); }
|
||
function spendCredit() { let c = parseInt(localStorage.getItem('ppcb_credits') || '5', 10); if (c <= 0) { alert('Out of download credits — contribute a curated part to earn 5 more.'); return false; } localStorage.setItem('ppcb_credits', --c); document.getElementById('cred-n').textContent = c; return true; }
|
||
|
||
// ---- footprint SVG from real pad geometry (top-down, copper pads, pad numbers, outline) ----
|
||
function footprintSVG(p) {
|
||
const pads = p.footprint_pads || []; if (!pads.length) return '<div class="hint">no footprint data</div>';
|
||
let minx = 1e9, miny = 1e9, maxx = -1e9, maxy = -1e9;
|
||
for (const d of pads) { minx = Math.min(minx, d.x - d.w / 2); maxx = Math.max(maxx, d.x + d.w / 2); miny = Math.min(miny, d.y - d.h / 2); maxy = Math.max(maxy, d.y + d.h / 2); }
|
||
const m = Math.max(maxx - minx, maxy - miny) * 0.14 + 0.4;
|
||
minx -= m; miny -= m; maxx += m; maxy += m;
|
||
const vb = `${minx} ${miny} ${maxx - minx} ${maxy - miny}`;
|
||
const fs = Math.max((maxx - minx), (maxy - miny)) * 0.035;
|
||
let s = `<svg viewBox="${vb}" preserveAspectRatio="xMidYMid meet" font-family="monospace">`;
|
||
s += `<rect x="${minx + m * 0.4}" y="${miny + m * 0.4}" width="${maxx - minx - m * 0.8}" height="${maxy - miny - m * 0.8}" fill="none" stroke="#bcbcb6" stroke-width="${fs * 0.12}" stroke-dasharray="${fs * 0.4} ${fs * 0.3}"/>`;
|
||
for (const d of pads) {
|
||
const round = (d.shape || '').toUpperCase().includes('OVAL') || (d.shape || '').toUpperCase().includes('ROUND');
|
||
s += `<rect x="${d.x - d.w / 2}" y="${d.y - d.h / 2}" width="${d.w}" height="${d.h}" rx="${round ? Math.min(d.w, d.h) / 2 : fs * 0.06}" fill="#c47a3d" stroke="#8a531f" stroke-width="${fs * 0.05}"/>`;
|
||
if (d.n != null) s += `<text x="${d.x}" y="${d.y + fs * 0.35}" font-size="${fs}" fill="#2a1a0c" text-anchor="middle" font-weight="600">${esc(d.n)}</text>`;
|
||
}
|
||
if (p.class === 'connector') { // board-edge marker on the mating side
|
||
s += `<line x1="${minx + m * 0.4}" y1="${maxy - m * 0.5}" x2="${maxx - m * 0.4}" y2="${maxy - m * 0.5}" stroke="#2f8f2f" stroke-width="${fs * 0.18}"/>`;
|
||
s += `<text x="${(minx + maxx) / 2}" y="${maxy - m * 0.5 - fs * 0.4}" font-size="${fs * 0.9}" fill="#2f8f2f" text-anchor="middle">BOARD EDGE</text>`;
|
||
}
|
||
return s + '</svg>';
|
||
}
|
||
|
||
// ---- schematic symbol from pins (labelled box; pins split L/R) ----
|
||
function schematicSVG(p) {
|
||
const pins = p.pins || []; if (!pins.length) return '<div class="hint">no pin data</div>';
|
||
const L = pins.filter((_, i) => i % 2 === 0), R = pins.filter((_, i) => i % 2 === 1);
|
||
const rows = Math.max(L.length, R.length), rh = 26, bw = 150, bx = 120, by = 40, bh = rows * rh + 20;
|
||
const W = bx + bw + 120, H = by + bh + 40;
|
||
let s = `<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" font-family="monospace">`;
|
||
s += `<rect x="${bx}" y="${by}" width="${bw}" height="${bh}" fill="#fdfdfb" stroke="#2a2a2a" stroke-width="2"/>`;
|
||
s += `<text x="${bx + bw / 2}" y="${by - 14}" font-size="15" text-anchor="middle" fill="#111" font-weight="700">${esc(p.mpn || p.id)}</text>`;
|
||
const draw = (arr, side) => arr.forEach((pin, i) => {
|
||
const y = by + 20 + i * rh, x0 = side === 'L' ? bx : bx + bw, x1 = side === 'L' ? bx - 34 : bx + bw + 34;
|
||
s += `<line x1="${x0}" y1="${y}" x2="${x1}" y2="${y}" stroke="#2a2a2a" stroke-width="2"/>`;
|
||
s += `<text x="${side === 'L' ? x1 - 5 : x1 + 5}" y="${y - 5}" font-size="11" fill="#666" text-anchor="${side === 'L' ? 'end' : 'start'}">${esc(pin.pin ?? '')}</text>`;
|
||
s += `<text x="${side === 'L' ? x0 + 8 : x0 - 8}" y="${y + 4}" font-size="12" fill="#111" text-anchor="${side === 'L' ? 'start' : 'end'}" font-weight="600">${esc(pin.name || '')}</text>`;
|
||
});
|
||
draw(L, 'L'); draw(R, 'R');
|
||
return s + '</svg>';
|
||
}
|
||
|
||
window.ppcbReady(async () => {
|
||
const parts = await window.getCatalog();
|
||
const p = parts.find((x) => x.id === id);
|
||
const root = document.getElementById('root');
|
||
if (!p) { root.innerHTML = '<div class="empty">Part not found. <a href="/">Back to library</a></div>'; return; }
|
||
document.title = `${p.mpn || p.id} — PromptPCB Parts`;
|
||
|
||
const params = (p.params || []).map((x) => `<tr><td>${esc(x.name)}</td><td class="v">${esc(x.value)} ${esc(x.unit || '')}${x.note ? `<span class="cond">${esc(x.note)}</span>` : ''}</td></tr>`).join('');
|
||
const pinsList = (p.pins || []).map((x) => `<div class="pin"><span class="n">${esc(x.pin ?? '')}</span>${esc(x.name || '')}<span class="t">${esc(x.type || '')}</span></div>`).join('');
|
||
const ucArr = p.use_cases || [];
|
||
const ucLi = (a) => a.map((u) => `<li>${esc(u)}</li>`).join('');
|
||
const ucs = ucArr.length ? `<ul class="uc">${ucLi(ucArr.slice(0, 5))}</ul>` +
|
||
(ucArr.length > 5 ? `<details class="more"><summary>+${ucArr.length - 5} more uses</summary><ul class="uc">${ucLi(ucArr.slice(5))}</ul></details>` : '') : '';
|
||
const tagList = Object.entries(p.tags || {}).filter(([, v]) => v != null && v !== '' && !Array.isArray(v) && typeof v !== 'boolean').map(([k, v]) => `<span class="tag"><b>${esc(k)}</b> ${esc(v)}</span>`).join('');
|
||
const kr = ((p.tags || {}).key_ratings || []).map((r) => `<span class="tag">${esc(r)}</span>`).join('');
|
||
const ser = p.series ? `<div class="tags" style="margin-top:8px">${Object.entries(p.series).filter(([k]) => k !== 'mesh').map(([k, v]) => `<span class="tag"><b>${esc(k)}</b> ${esc(v)}</span>`).join('')}</div>` : '';
|
||
const edge = p.board_edge ? `<div class="viznote"><b>Board edge:</b> ${esc(p.board_edge)}.</div>` : '';
|
||
|
||
root.innerHTML = `<div class="detail">
|
||
<div><div class="viz">
|
||
<div class="viztabs"><button data-tab="d3" class="on">3D model</button><button data-tab="sch">Schematic</button><button data-tab="fp">Footprint</button></div>
|
||
<div class="vizpanel" data-v="d3"><canvas id="cv"></canvas><span class="hint">real manufacturer model · drag to orbit</span></div>
|
||
<div class="vizpanel" data-v="sch" hidden>${schematicSVG(p)}</div>
|
||
<div class="vizpanel" data-v="fp" hidden>${footprintSVG(p)}<span class="hint">${esc(p.package || '')}</span></div>
|
||
${edge}
|
||
</div></div>
|
||
<div>
|
||
<div class="dclass">${esc(p.class)}${p.type === 'series' ? ' · series' : ''}</div>
|
||
<h1 class="dh1">${esc((p.role || p.summary || '').split('—')[0])}</h1>
|
||
<div class="dmpn">${esc(p.mpn || p.id)} · ${esc(p.manufacturer || '')} · ${esc(p.lcsc || '')}</div>
|
||
<p style="color:var(--dim);line-height:1.6;margin:0 0 8px">${esc(p.summary)}</p>
|
||
<div class="dactions">
|
||
<button class="btn" id="dl-all">↓ Download files</button>
|
||
<a class="btn ghost" href="${esc(p.datasheet_url)}" target="_blank" rel="noopener">Datasheet ↗</a>
|
||
<button class="btn ghost" id="lib">+ My library</button>
|
||
</div>
|
||
<div class="assets"><span><span class="ok">✓</span> 3D model</span><span><span class="ok">✓</span> Schematic</span><span><span class="ok">✓</span> Footprint</span><span><span class="ok">✓</span> SPICE</span><span><span class="ok">✓</span> ${(p.pins || []).length} pins</span></div>
|
||
${p.when_to_use ? `<div class="section"><h3>When to use</h3><p style="color:var(--text);line-height:1.6;margin:0">${esc(p.when_to_use)}</p></div>` : ''}
|
||
${ucs ? `<div class="section"><h3>What it's used for</h3>${ucs}</div>` : ''}
|
||
${params ? `<div class="section"><h3>Specifications</h3><table class="ptable">${params}</table></div>` : ''}
|
||
${ser}
|
||
${pinsList ? `<div class="section"><h3>Pins & pads</h3><div class="pins">${pinsList}</div></div>` : ''}
|
||
${(tagList || kr) ? `<div class="section"><h3>Requirement tags</h3><div class="tags">${kr}${tagList}</div></div>` : ''}
|
||
${p.spice ? `<div class="section"><h3>SPICE model</h3><details class="more"><summary>show model</summary><pre class="spice">${esc(p.spice)}</pre></details></div>` : ''}
|
||
</div>
|
||
</div>`;
|
||
|
||
// tab switching (lazy-mount the 3D viewer on first show)
|
||
let mounted = false;
|
||
const show = (tab) => {
|
||
root.querySelectorAll('.viztabs button').forEach((b) => b.classList.toggle('on', b.dataset.tab === tab));
|
||
root.querySelectorAll('.vizpanel').forEach((v) => (v.hidden = v.dataset.v !== tab));
|
||
if (tab === 'd3' && !mounted) { mounted = true; mountViewer(document.getElementById('cv'), p.model_3d); }
|
||
};
|
||
root.querySelectorAll('.viztabs button').forEach((b) => (b.onclick = () => show(b.dataset.tab)));
|
||
show('d3');
|
||
|
||
document.getElementById('dl-all').onclick = async () => {
|
||
try { const wrl = await (await fetch(p.model_3d)).text(); dl(`${p.id}.wrl`, wrl); } catch {}
|
||
dl(`${p.id}.cir`, `* ${p.mpn} SPICE model — PromptPCB Parts\n${p.spice || ''}\n`);
|
||
dl(`${p.id}.json`, JSON.stringify(p, null, 2), 'application/json');
|
||
};
|
||
document.getElementById('lib').onclick = () => {
|
||
const lib = JSON.parse(localStorage.getItem('ppcb_lib') || '[]');
|
||
if (!lib.includes(p.id)) { lib.push(p.id); localStorage.setItem('ppcb_lib', JSON.stringify(lib)); }
|
||
document.getElementById('lib').textContent = '✓ In library';
|
||
};
|
||
});
|