// 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 '
no footprint data
'; 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 = ``; s += ``; for (const d of pads) { const round = (d.shape || '').toUpperCase().includes('OVAL') || (d.shape || '').toUpperCase().includes('ROUND'); s += ``; if (d.n != null) s += `${esc(d.n)}`; } if (p.class === 'connector') { // board-edge marker on the mating side s += ``; s += `BOARD EDGE`; } return s + ''; } // ---- schematic symbol from pins (labelled box; pins split L/R) ---- function schematicSVG(p) { const pins = p.pins || []; if (!pins.length) return '
no pin data
'; 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 = ``; s += ``; s += `${esc(p.mpn || p.id)}`; 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 += ``; s += `${esc(pin.pin ?? '')}`; s += `${esc(pin.name || '')}`; }); draw(L, 'L'); draw(R, 'R'); return s + ''; } 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 = '
Part not found. Back to library
'; return; } document.title = `${p.mpn || p.id} — PromptPCB Parts`; const params = (p.params || []).map((x) => `${esc(x.name)}${esc(x.value)} ${esc(x.unit || '')}${x.note ? `${esc(x.note)}` : ''}`).join(''); const pinsList = (p.pins || []).map((x) => `
${esc(x.pin ?? '')}${esc(x.name || '')}${esc(x.type || '')}
`).join(''); const ucArr = p.use_cases || []; const ucLi = (a) => a.map((u) => `
  • ${esc(u)}
  • `).join(''); const ucs = ucArr.length ? `` + (ucArr.length > 5 ? `
    +${ucArr.length - 5} more uses
    ` : '') : ''; const tagList = Object.entries(p.tags || {}).filter(([, v]) => v != null && v !== '' && !Array.isArray(v) && typeof v !== 'boolean').map(([k, v]) => `${esc(k)} ${esc(v)}`).join(''); const kr = ((p.tags || {}).key_ratings || []).map((r) => `${esc(r)}`).join(''); const ser = p.series ? `
    ${Object.entries(p.series).filter(([k]) => k !== 'mesh').map(([k, v]) => `${esc(k)} ${esc(v)}`).join('')}
    ` : ''; const edge = p.board_edge ? `
    Board edge: ${esc(p.board_edge)}.
    ` : ''; root.innerHTML = `
    real manufacturer model · drag to orbit
    ${edge}
    ${esc(p.class)}${p.type === 'series' ? ' · series' : ''}

    ${esc((p.role || p.summary || '').split('—')[0])}

    ${esc(p.mpn || p.id)} · ${esc(p.manufacturer || '')} · ${esc(p.lcsc || '')}

    ${esc(p.summary)}

    Datasheet ↗
    ✓ 3D model✓ Schematic✓ Footprint✓ SPICE✓ ${(p.pins || []).length} pins
    ${p.when_to_use ? `

    When to use

    ${esc(p.when_to_use)}

    ` : ''} ${ucs ? `

    What it's used for

    ${ucs}
    ` : ''} ${params ? `

    Specifications

    ${params}
    ` : ''} ${ser} ${pinsList ? `

    Pins & pads

    ${pinsList}
    ` : ''} ${(tagList || kr) ? `

    Requirement tags

    ${kr}${tagList}
    ` : ''} ${p.spice ? `

    SPICE model

    show model
    ${esc(p.spice)}
    ` : ''}
    `; // 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'; }; });