app
Component Widgets
Public Made by Adomby adom
Reusable component widgets for pinouts, thermal vias, molecule relationships, reviewed chip outlines and lightweight four-level LOD cache listings.
← Commit history
pinout: keep the end pin of each side inside the body
At 12 pins a side the inferred quad layout distributed them across the full
inner..outer span while the body is drawn inset 16 from the outermost pins, so
the first and last pin of every side landed 4.3px past the corner and collided
with the end pin of the neighbouring side: 1 into 48, 12 into 13, 24 into 25,
36 into 37.
Real quad packages have a corner keep-out, which is why the xy path never showed
this and only the inferred layout did. The run is now inset to match, on the DIP
branch too.
1 file changed
+69−10
widgets/pinout-widget.js+69−10@@ -61,22 +61,29 @@ return { ...p, x, y, side }; }); } else {+ // The body is drawn inset 16 from the outermost pins, so a run distributed+ // across the whole inner..outer span puts the FIRST and LAST pin of each+ // side outside the body: at 12 per side they landed 4.3px past each corner+ // and collided with the end pin of the neighbouring side (1 into 48, 12+ // into 13, and so on). Real quad packages have a corner keep-out anyway,+ // which is why the xy path never showed this. Inset the run to match. const N = edge.length, inner = pad, outer = S - pad, at = (a, b, t) => a + (b - a) * t;+ const CORNER = 26, runLo = inner + CORNER, runHi = outer - CORNER; const quad = forced === 'quad' || (forced !== 'dual' && /QF|BGA|LGA|LCC|QFN|QFP/i.test(pkg || '')); if (quad) { const per = Math.ceil(N / 4); laid = edge.map((p, i) => { const s = Math.floor(i / per), t = ((i % per) + 0.5) / per;- if (s === 0) return { ...p, x: inner, y: at(inner, outer, t), side: 'left' };- if (s === 1) return { ...p, x: at(inner, outer, t), y: outer, side: 'bottom' };- if (s === 2) return { ...p, x: outer, y: at(outer, inner, t), side: 'right' };- return { ...p, x: at(outer, inner, t), y: inner, side: 'top' };+ if (s === 0) return { ...p, x: inner, y: at(runLo, runHi, t), side: 'left' };+ if (s === 1) return { ...p, x: at(runLo, runHi, t), y: outer, side: 'bottom' };+ if (s === 2) return { ...p, x: outer, y: at(runHi, runLo, t), side: 'right' };+ return { ...p, x: at(runHi, runLo, t), y: inner, side: 'top' }; }); } else { const half = Math.ceil(N / 2); // DIP-style: left top→bottom, right bottom→top laid = edge.map((p, i) => i < half- ? { ...p, x: inner, y: at(inner, outer, (i + 0.5) / half), side: 'left' }- : { ...p, x: outer, y: at(outer, inner, (i - half + 0.5) / (N - half)), side: 'right' });+ ? { ...p, x: inner, y: at(runLo, runHi, (i + 0.5) / half), side: 'left' }+ : { ...p, x: outer, y: at(runHi, runLo, (i - half + 0.5) / (N - half)), side: 'right' }); } } centers.forEach(p => laid.push({ ...p, x: mid, y: mid, side: 'center' }));@@ -115,7 +122,7 @@ const heading = this.getAttribute('heading') || d.mpn || ''; root.innerHTML = `<style>${this._css()}</style> <div class="card">- ${heading ? `<div class="hd"><span class="mpn">${esc(heading)}</span>${d.package ? `<span class="pkg">${esc(d.package)}</span>` : ''}<span class="count">${pins.length} pins</span></div>` : ''}+ <div class="hd">${heading ? `<span class="mpn">${esc(heading)}</span>` : ''}${d.package ? `<span class="pkg">${esc(d.package)}</span>` : ''}<span class="count">${pins.length} pins</span><span class="tools"><button class="tbtn" data-act="copy" title="Copy the pin map as JSON">copy</button><button class="tbtn" data-act="dl" title="Download the pin map as JSON">JSON ↓</button></span></div> <div class="body"> <div class="diagram"> <div class="readout" data-readout>hover a pin →</div>@@ -189,6 +196,35 @@ /** Public: drive the highlight from outside (e.g. a linked footprint/symbol widget). */ highlight(n) { this._set(n, false); } + /** Public: the canonical pin map as a plain object — the structured handle for code/AI.+ * Types are resolved (inferred filled in) so consumers get a complete map. No fabricated fields. */+ toJSON() {+ const d = this._data || {};+ const pins = (Array.isArray(d.pins) ? d.pins : [])+ .map(p => ({ number: p.number, name: p.name, type: (p.type || inferType(p.name)).toLowerCase(), ...(p.xy ? { xy: p.xy } : {}) }))+ .sort((a, b) => (Number(a.number) - Number(b.number)) || String(a.number).localeCompare(String(b.number)));+ const out = {};+ if (d.mpn) out.mpn = d.mpn;+ if (d.package) out.package = d.package;+ out.pins = pins;+ return out;+ }++ _exportJson() { return JSON.stringify(this.toJSON(), null, 2); }+ _exportName() { const d = this._data || {}; return ((d.mpn || 'pinout') + (d.package ? '-' + d.package : '')).replace(/[^\w.-]+/g, '_') + '.pins.json'; }+ _download() {+ const blob = new Blob([this._exportJson()], { type: 'application/json' });+ const url = URL.createObjectURL(blob), a = document.createElement('a');+ a.href = url; a.download = this._exportName(); document.body.appendChild(a); a.click();+ a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000);+ }+ async _copy(btn) {+ const txt = this._exportJson(), done = () => { if (!btn) return; const o = btn.textContent; btn.textContent = 'copied'; btn.classList.add('ok'); setTimeout(() => { btn.textContent = o; btn.classList.remove('ok'); }, 1100); };+ try { await navigator.clipboard.writeText(txt); done(); return; } catch (e) { /* sandboxed / denied — fall back */ }+ try { const ta = document.createElement('textarea'); ta.value = txt; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); done(); }+ catch (e2) { this._download(); } // last resort: hand them the file+ }+ // adom-lbr shared cross-highlight bus: BroadcastChannel('lbr-hover') + parent relay. _busInit() { this._self = 'w' + Math.random().toString(36).slice(2);@@ -207,8 +243,18 @@ _wire(pins) { this._pinsRendered = pins; const root = this.shadowRoot;+ const dl = root.querySelector('.tbtn[data-act="dl"]'); if (dl) dl.onclick = () => this._download();+ const cp = root.querySelector('.tbtn[data-act="copy"]'); if (cp) cp.onclick = () => this._copy(cp); root.querySelectorAll('tr[data-pin]').forEach(r => { r.onmouseenter = () => this._set(r.dataset.pin, true); r.onmouseleave = () => this._set('', true); }); root.querySelectorAll('svg .pin[data-pin]').forEach(g => { g.onmouseenter = () => this._set(g.dataset.pin, true); g.onmouseleave = () => this._set('', true); });+ // Match the pin table's height to the chip diagram column so it fills the+ // space and scrolls, instead of a fixed short box next to a taller chip.+ const dia = root.querySelector('.diagram'), tw = root.querySelector('.tablewrap');+ if (dia && tw) {+ const fit = () => { const h = dia.getBoundingClientRect().height; if (h > 40) tw.style.maxHeight = Math.round(h) + 'px'; };+ requestAnimationFrame(fit); setTimeout(fit, 60);+ try { if (this._ro) this._ro.disconnect(); this._ro = new ResizeObserver(fit); this._ro.observe(dia); } catch (e) { /* */ }+ } } _css() {@@ -221,13 +267,20 @@ :host([theme="light"]){--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88;--hot:rgba(13,143,136,.10)} *{box-sizing:border-box} .card{background:var(--bg);border:1px solid var(--line);border-radius:14px;padding:16px;color:var(--text);- font:14px/1.5 -apple-system,'Segoe UI',Roboto,Arial,sans-serif}+ font:14px/1.5 -apple-system,'Segoe UI',Roboto,Arial,sans-serif;container-type:inline-size} .hd{display:flex;align-items:baseline;gap:10px;margin-bottom:12px} .hd .mpn{font:700 17px ui-monospace,'SF Mono',Menlo,monospace;letter-spacing:-.2px} .hd .pkg{font:600 11px ui-monospace,monospace;color:var(--accent);background:color-mix(in srgb,var(--accent) 12%,transparent);border:1px solid color-mix(in srgb,var(--accent) 30%,transparent);padding:2px 8px;border-radius:6px} .hd .count{margin-left:auto;font:12px ui-monospace,monospace;color:var(--dim2)}+ .hd .tools{display:flex;gap:6px;align-self:center}+ .tbtn{font:600 10.5px ui-monospace,monospace;letter-spacing:.03em;color:var(--dim);background:var(--panel);+ border:1px solid var(--line);border-radius:6px;padding:3px 8px;cursor:pointer;transition:color .12s,border-color .12s,background .12s;white-space:nowrap}+ .tbtn:hover{color:var(--text);border-color:var(--line2)}+ .tbtn.ok{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 45%,transparent)} .body{display:grid;grid-template-columns:minmax(300px,1fr) minmax(220px,1fr);gap:20px;align-items:start}- @media(max-width:620px){.body{grid-template-columns:1fr}}+ /* stack diagram over table when the WIDGET (not the viewport) is narrow —+ e.g. side-by-side in a cross-link row — so the table never clips */+ @container (max-width:580px){.body{grid-template-columns:1fr}} .readout{height:24px;display:flex;align-items:center;gap:9px;font:600 13px ui-monospace,monospace;color:var(--dim);padding:0 4px} .readout b{color:var(--accent)} .readout .rt{margin-left:auto;font-size:11px;text-transform:uppercase;letter-spacing:.08em} .diagram{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:8px}@@ -242,7 +295,13 @@ .pin{cursor:default} .legend{display:flex;flex-wrap:wrap;gap:13px;margin:8px 2px 2px;font-size:11.5px;color:var(--dim)} .legend i{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px;vertical-align:middle}- .tablewrap{max-height:440px;overflow:auto;border:1px solid var(--line);border-radius:10px}+ .tablewrap{max-height:440px;overflow:auto;border:1px solid var(--line);border-radius:10px;scrollbar-width:thin;scrollbar-color:var(--line2) transparent}+ .tablewrap::-webkit-scrollbar{width:9px;height:9px}+ .tablewrap::-webkit-scrollbar-track{background:transparent}+ .tablewrap::-webkit-scrollbar-thumb{background:var(--line2);border-radius:9px;border:2px solid transparent;background-clip:padding-box}+ .tablewrap::-webkit-scrollbar-thumb:hover{background:var(--dim2);border:2px solid transparent;background-clip:padding-box}+ .tablewrap::-webkit-scrollbar-button{display:none;width:0;height:0}+ .tablewrap::-webkit-scrollbar-corner{background:transparent} table{width:100%;border-collapse:collapse;font-size:12.5px} thead th{position:sticky;top:0;background:var(--panel);text-align:left;color:var(--dim2);font:600 10px ui-monospace,monospace;letter-spacing:.08em;text-transform:uppercase;padding:7px 9px;border-bottom:1px solid var(--line)} td{padding:4px 9px;border-bottom:1px solid color-mix(in srgb,var(--line) 55%,transparent);font-family:ui-monospace,monospace}