With real geometry in, a fixed label size turns multi-character names on fine-pitch pads into overlapping mush — USB-C's A1/B12 row was unreadable. Scale per pad instead, bounded by both the pad width (divided across the label) and its height.
132 lines
9.9 KiB
JavaScript
132 lines
9.9 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) {
|
||
// EasyEDA names a round pad ELLIPSE; OVAL/ROUND are the KiCad-side spellings.
|
||
const round = /OVAL|ROUND|ELLIPSE|CIRCLE/.test((d.shape || '').toUpperCase());
|
||
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}"/>`;
|
||
// Fit the label to its pad. A fixed size turns multi-character names on fine-pitch
|
||
// pads (USB-C's A1/B12) into overlapping mush, so narrow pads get smaller text.
|
||
if (d.n != null) {
|
||
const lab = String(d.n);
|
||
const pf = Math.min(fs, (d.w * 1.7) / Math.max(lab.length, 1), d.h * 0.95);
|
||
s += `<text x="${d.x}" y="${d.y + pf * 0.35}" font-size="${pf}" fill="#2a1a0c" text-anchor="middle" font-weight="600">${esc(lab)}</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">${[
|
||
['3D model', !!p.model_3d],
|
||
['Schematic', !!(p.pins || []).length], // schematicSVG() draws from pins
|
||
['Footprint', !!(p.footprint_pads || []).length], // footprintSVG() draws from footprint_pads
|
||
['SPICE', !!p.spice],
|
||
[`${(p.pins || []).length} pins`, !!(p.pins || []).length],
|
||
].map(([label, have]) => `<span><span class="${have ? 'ok' : 'hint'}">${have ? '✓' : '—'}</span> ${label}</span>`).join('')}</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';
|
||
};
|
||
});
|