parts-store/assets/part.js

119 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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 &amp; 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';
};
});