by adom
0603 SMD Green LED · 3.2 V drop · 25 mA max
master
John Lauer
Pads: square corners, as the datasheet draws them (no added rounding); viewers, lbr, insertion GLB, hero rebuilt from t…
644c9a7
16d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IN-S63BTG</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0d1117;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
color: #e6edf3;
}
.toolbar {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: #161b22;
border-bottom: 1px solid #21262d;
flex-shrink: 0;
font-size: 13px;
}
.toolbar .title {
font-weight: 600;
color: #00b8b0;
font-size: 15px;
letter-spacing: 0.02em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.toolbar .badge {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
background: rgba(0, 184, 176, 0.12);
color: #00b8b0;
border: 1px solid rgba(0, 184, 176, 0.25);
margin-left: 6px;
}
.toolbar .spacer { flex: 1; }
.toolbar .sep {
width: 1px;
height: 22px;
background: #21262d;
margin: 0 4px;
flex-shrink: 0;
}
.toolbar .btn-group {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
.toolbar button {
background: rgba(0, 184, 176, 0.12);
border: 1px solid rgba(0, 184, 176, 0.25);
border-radius: 4px;
padding: 4px 10px;
cursor: pointer;
font-size: 12px;
color: #8b949e;
transition: all 0.15s;
}
.toolbar button:hover { background: rgba(0,184,176,0.18); color: #e6edf3; border-color: rgba(0,184,176,0.4); }
.toolbar button:active { background: rgba(0,184,176,0.28); }
.toolbar button.active { background: rgba(0,184,176,0.2); color: #00b8b0; border-color: rgba(0,184,176,0.4); }
.toolbar .zoom-level {
font-size: 11px;
color: #8b949e;
min-width: 44px;
text-align: center;
font-variant-numeric: tabular-nums;
}
.canvas-wrap {
flex: 1;
overflow: hidden;
position: relative;
cursor: grab;
background-color: #0d1117;
background-image:
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
background-size: 20px 20px;
}
.canvas-wrap.dragging { cursor: grabbing; }
.canvas-wrap.no-grid { background-image: none; }
.canvas {
position: absolute;
transform-origin: 0 0;
}
.canvas svg { display: block; }
.tooltip {
display: none;
position: fixed;
background: #21262d;
backdrop-filter: blur(12px);
color: #e6edf3;
padding: 10px 14px;
border-radius: 8px;
border: 1px solid rgba(0, 184, 176, 0.25);
font-size: 12px;
line-height: 1.6;
pointer-events: none;
z-index: 100;
max-width: 280px;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
.tooltip.visible { display: block; }
.tooltip .tt-pad { font-weight: 700; font-size: 15px; color: #00e6dc; }
.tooltip .tt-signal { color: #f0c040; font-size: 13px; margin-left: 6px; font-weight: 600; }
.tooltip .tt-dims {
display: inline-block;
margin-top: 4px;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
background: rgba(200,52,52,0.15);
color: #C83434;
border: 1px solid rgba(200,52,52,0.3);
}
.tooltip .tt-desc {
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid #21262d;
color: #8b949e;
font-size: 11px;
line-height: 1.5;
}
.tooltip.sticky {
pointer-events: auto;
user-select: text;
border-color: #00e6dc;
}
.tooltip .tt-hint {
margin-top: 6px;
font-size: 9px;
color: #484f58;
text-align: center;
}
.tooltip .tt-source {
margin-top: 6px;
padding-top: 4px;
border-top: 1px solid #21262d;
font-size: 9px;
}
.tooltip .tt-source-label { color: #8C6BF7; }
.tooltip .tt-source a { color: #58a6ff; text-decoration: none; }
.tooltip .tt-source a:hover { text-decoration: underline; }
.edit-btn {
position: absolute; top: 10px; right: 10px; z-index: 100;
display: flex; align-items: center; gap: 6px;
padding: 5px 10px;
background: rgba(22,27,34,0.75);
backdrop-filter: blur(8px);
border: 1px solid rgba(140,107,247,0.2);
border-radius: 6px;
color: #8b949e; font-size: 11px;
cursor: pointer; text-decoration: none;
transition: all 0.2s; opacity: 0.7;
}
.edit-btn:hover {
opacity: 1;
background: rgba(140,107,247,0.12);
border-color: rgba(140,107,247,0.4);
color: #C5B3FF;
}
.edit-btn svg { width: 12px; height: 12px; }
/* Regen prompt modal */
.regen-overlay {
display: none;
position: fixed; inset: 0;
background: rgba(0,0,0,0.6);
z-index: 2000;
align-items: center; justify-content: center;
}
.regen-overlay.visible { display: flex; }
.regen-modal {
background: #161b22;
border: 1px solid #21262d;
border-radius: 10px;
padding: 20px 24px;
max-width: 520px;
width: 90%;
box-shadow: 0 12px 40px rgba(0,0,0,0.5);
}
.regen-modal h3 {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
color: #00b8b0;
}
.regen-modal .prompt-text {
background: undefined;
border: 1px solid #21262d;
border-radius: 6px;
padding: 12px 14px;
font-size: 13px;
line-height: 1.5;
color: undefined;
white-space: pre-wrap;
word-break: break-word;
user-select: all;
}
.regen-modal .modal-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 14px;
}
.regen-modal .copy-status {
font-size: 11px;
color: #484f58;
}
.regen-modal .copy-status.copied { color: #00b8b0; }
.regen-modal .modal-btns { display: flex; gap: 8px; }
.regen-modal .modal-btns button {
padding: 6px 14px;
border-radius: 6px;
border: 1px solid #21262d;
background: #161b22;
color: #8b949e;
font-size: 12px;
cursor: pointer;
}
.regen-modal .modal-btns button:hover { background: rgba(255,255,255,0.06); }
.regen-modal .modal-btns .copy-btn {
background: rgba(0,184,176,0.12);
border-color: rgba(0,184,176,0.3);
color: #00b8b0;
}
.regen-modal .modal-btns .copy-btn:hover { background: rgba(0,184,176,0.22); }
/* Pad highlight */
.pad-highlight {
stroke: #00b8b0 !important;
stroke-width: 0.15 !important;
fill-opacity: 0.6 !important;
}
.bottom-panel {
background: #161b22;
border-top: 1px solid #21262d;
flex-shrink: 0;
}
.info-row {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 1px 0;
padding: 5px 8px;
font-size: 10px;
}
.info-item {
display: inline-flex;
align-items: baseline;
gap: 5px;
padding: 1px 0;
margin-right: 8px;
}
.info-item::after {
content: '';
display: inline-block;
width: 1px;
height: 10px;
background: #21262d;
margin-left: 8px;
vertical-align: middle;
}
.info-item:last-child::after { display: none; }
.info-item:last-child { margin-right: 0; }
.info-label {
color: #484f58;
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 9px;
}
.info-value { color: #8b949e; }
.info-value a { color: #00b8b0; text-decoration: none; }
.info-value a:hover { text-decoration: underline; }
.info-divider {
height: 1px;
background: #21262d;
margin: 0 8px;
}
.coord-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
font-size: 10px;
color: #8b949e;
font-variant-numeric: tabular-nums;
}
</style>
</head>
<body>
<div class="toolbar">
<span class="title">IN-S63BTG</span>
<span class="badge">Footprint</span>
<div class="spacer"></div>
<div class="btn-group">
<button onclick="zoomOut()">−</button>
<span class="zoom-level" id="zoom-level">100%</span>
<button onclick="zoomIn()">+</button>
</div>
<div class="sep"></div>
<button onclick="zoomFit()">Fit</button>
<button onclick="zoomReset()">1:1</button>
<button onclick="toggleGrid()" id="grid-btn" class="active">Grid</button>
<button onclick="toggleSolder()" id="solder-btn" class="active" title="250µm solder jet blobs for paste jetting at the 3rd party fab, which uses this placement data for its jetting sequence. Blob count, position, and size (250–600µm) are adjustable. Placement accuracy: ±50µm.">Solder</button>
<button onclick="toggleLabels()" id="labels-btn">Labels</button>
<div class="sep"></div>
<button onclick="showRegenPrompt()" id="regen-btn" title="Show regeneration prompt to paste into Claude Code">Regen</button>
</div>
<div class="regen-overlay" id="regen-overlay" onclick="if(event.target===this)closeRegenPrompt()">
<div class="regen-modal">
<h3>Regeneration Prompt</h3>
<div class="prompt-text" id="regen-prompt-text"></div>
<div class="modal-footer">
<span class="copy-status" id="regen-copy-status">Copied to clipboard</span>
<div class="modal-btns">
<button onclick="closeRegenPrompt()">Close</button>
<button class="copy-btn" onclick="copyRegenText()">Copy</button>
</div>
</div>
</div>
</div>
<div class="canvas-wrap" id="canvas-wrap">
<div class="canvas" id="canvas">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4.5 3.4">
<rect x="0" y="0" width="4.5" height="3.4" fill="#0d1117"/>
<rect x="1" y="1" width="2.5" height="1.4" fill="none" stroke="#FF26E2" stroke-width="0.05" stroke-dasharray="0.2,0.1"/>
<rect x="1.45" y="1.3" width="1.6" height="0.8" fill="none" stroke="#6e7681" stroke-width="0.1"/>
<line x1="1.7" y1="1.3" x2="1.7" y2="2.1" stroke="#6e7681" stroke-width="0.1"/>
<line x1="1.09" y1="1.14" x2="3.05" y2="1.14" stroke="#00cccc" stroke-width="0.12" stroke-linecap="round"/><line x1="1.09" y1="2.26" x2="3.05" y2="2.26" stroke="#00cccc" stroke-width="0.12" stroke-linecap="round"/><line x1="1.09" y1="1.14" x2="1.09" y2="2.26" stroke="#00cccc" stroke-width="0.12" stroke-linecap="round"/>
<rect x="1.25" y="1.25" width="0.6" height="0.9" rx="0" ry="0" fill="#C83434" stroke="#ef5350" stroke-width="0.04"/>
<text x="1.55" y="1.7" text-anchor="middle" dominant-baseline="central" font-size="0.270" fill="#fff" font-family="sans-serif">1</text>
<rect x="2.65" y="1.25" width="0.6" height="0.9" rx="0" ry="0" fill="#C83434" stroke="#ef5350" stroke-width="0.04"/>
<text x="2.95" y="1.7" text-anchor="middle" dominant-baseline="central" font-size="0.270" fill="#fff" font-family="sans-serif">2</text>
</svg>
</div>
<a class="edit-btn" href="https://coder.john-service-wiki-a054078430ce38e4.containers.adom.inc/proxy/8785/wiki/components/in-s63btg/edit?field=content" target="_blank" title="Suggest corrections on Adom Wiki">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M11.5 1.5l3 3L5 14H2v-3L11.5 1.5z"/>
<path d="M9.5 3.5l3 3"/>
</svg>
Suggest edit
</a>
</div>
<div class="tooltip" id="tooltip"></div>
<div class="bottom-panel">
<div class="info-row">
<div class="info-item"><span class="info-label">Desc</span><span class="info-value">Land pattern: Inolux IN-S63BT series datasheet V1.2, p.1 (Recommended Solder Pattern)</span></div><div class="info-item"><span class="info-label">Pads</span><span class="info-value">2 · 0.6 × 0.9 mm, r 0 mm</span></div><div class="info-item"><span class="info-label">Body</span><span class="info-value">1.6 × 0.8 mm</span></div><div class="info-item"><span class="info-label">Courtyard</span><span class="info-value">2.5 × 1.4 mm</span></div>
</div>
<div class="info-divider"></div>
<div class="coord-row">
<span id="cursor-pos">X: 0.00 Y: 0.00 mm</span>
</div>
</div>
<script>
const pads = [{"number": "1", "signal": "", "x": -0.7, "y": 0.0, "width": 0.6, "height": 0.9, "shape": "roundrect", "mountType": "smd", "rotation": 0, "desc": ""}, {"number": "2", "signal": "", "x": 0.7, "y": 0.0, "width": 0.6, "height": 0.9, "shape": "roundrect", "mountType": "smd", "rotation": 0, "desc": ""}];
const svgW = 4.5;
const svgH = 3.4;
const svgVbX = 0;
const svgVbY = 0;
const offsetX = 2.25;
const offsetY = 1.7;
const datasheetUrl = "";
const _partName = "";
const _fpName = "IN-S63BTG";
const _kicadModPath = "/home/adom/project/project-content/schematics/footprints/IN-S63BTG/IN-S63BTG.kicad_mod";
const wrap = document.getElementById('canvas-wrap');
const canvas = document.getElementById('canvas');
const tooltip = document.getElementById('tooltip');
const zoomLabel = document.getElementById('zoom-level');
const cursorPos = document.getElementById('cursor-pos');
let scale = 1, panX = 0, panY = 0;
let isDragging = false;
let dragStartX, dragStartY, panStartX, panStartY;
let gridVisible = true;
const MM_TO_PX = 3.78;
const svgPxW = svgW * MM_TO_PX;
const svgPxH = svgH * MM_TO_PX;
const svgEl = canvas.querySelector('svg');
if (svgEl) {
svgEl.setAttribute('width', svgPxW + 'px');
svgEl.setAttribute('height', svgPxH + 'px');
}
function updateTransform() {
canvas.style.transform = 'translate(' + panX + 'px,' + panY + 'px) scale(' + scale + ')';
zoomLabel.textContent = Math.round(scale * 100) + '%';
}
function zoomFit() {
const ww = wrap.clientWidth, wh = wrap.clientHeight;
if (ww === 0 || wh === 0) return; // iframe not laid out yet — ResizeObserver will retry
const margin = 24;
// Use getBBox() to fit to visible content only (excludes hidden labels)
let fitW = svgPxW, fitH = svgPxH, fitX = 0, fitY = 0;
if (svgEl) {
try {
const bb = svgEl.getBBox();
if (bb.width > 0 && bb.height > 0) {
fitW = bb.width * MM_TO_PX;
fitH = bb.height * MM_TO_PX;
fitX = (bb.x - svgVbX) * MM_TO_PX;
fitY = (bb.y - svgVbY) * MM_TO_PX;
}
} catch {}
}
scale = Math.min((ww - margin * 2) / fitW, (wh - margin * 2) / fitH, 100);
panX = (ww - fitW * scale) / 2 - fitX * scale;
panY = (wh - fitH * scale) / 2 - fitY * scale;
updateTransform();
}
function zoomReset() {
scale = 1;
panX = (wrap.clientWidth - svgPxW) / 2;
panY = (wrap.clientHeight - svgPxH) / 2;
updateTransform();
}
function zoomIn() { zoomAt(wrap.clientWidth / 2, wrap.clientHeight / 2, 1.25); }
function zoomOut() { zoomAt(wrap.clientWidth / 2, wrap.clientHeight / 2, 0.8); }
function zoomAt(cx, cy, factor) {
const ns = Math.max(0.1, Math.min(scale * factor, 40));
const r = ns / scale;
panX = cx - (cx - panX) * r;
panY = cy - (cy - panY) * r;
scale = ns;
updateTransform();
}
function toggleGrid() {
gridVisible = !gridVisible;
wrap.classList.toggle('no-grid', !gridVisible);
document.getElementById('grid-btn').classList.toggle('active', gridVisible);
}
// ── Solder blob jetting layer ──
let solderVisible = true;
let solderGroup = null;
function setupSolderBlobs() {
if (!svgEl) return;
solderGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
solderGroup.setAttribute('id', 'solder-layer');
solderGroup.style.display = '';
solderGroup.style.pointerEvents = 'none';
const BLOB_RADIUS = 0.125; // 250µm diameter / 2
const BLOB_SPACING = 0.25; // center-to-center (blobs touching — fills the pad)
pads.forEach(pad => {
if (pad.mountType !== 'smd') return;
const cx = pad.x + offsetX;
const cy = pad.y + offsetY;
const rot = (pad.rotation || 0) * Math.PI / 180;
// Each axis computed independently: if pad is wide enough, grid; otherwise single centered blob
const inset = BLOB_RADIUS; // blob edges align with pad edges — no overhang
const availW = pad.width - 2 * inset;
const availH = pad.height - 2 * inset;
// At minimum 1 blob per axis — even narrow pads get a centered row/column
const nCols = availW > 0 ? Math.max(1, Math.floor(availW / BLOB_SPACING) + 1) : 1;
const nRows = availH > 0 ? Math.max(1, Math.floor(availH / BLOB_SPACING) + 1) : 1;
const spanW = availW > 0 ? availW : 0;
const spanH = availH > 0 ? availH : 0;
const stepX = nCols > 1 ? spanW / (nCols - 1) : 0;
const stepY = nRows > 1 ? spanH / (nRows - 1) : 0;
const startX = nCols > 1 ? -spanW / 2 : 0;
const startY = nRows > 1 ? -spanH / 2 : 0;
const cosR = Math.cos(rot);
const sinR = Math.sin(rot);
for (let row = 0; row < nRows; row++) {
for (let col = 0; col < nCols; col++) {
const lx = startX + col * stepX;
const ly = startY + row * stepY;
const rx = lx * cosR - ly * sinR;
const ry = lx * sinR + ly * cosR;
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', (cx + rx).toFixed(4));
circle.setAttribute('cy', (cy + ry).toFixed(4));
circle.setAttribute('r', BLOB_RADIUS.toFixed(4));
circle.setAttribute('fill', '#D4AA70');
circle.setAttribute('fill-opacity', '0.75');
circle.setAttribute('stroke', '#B8860B');
circle.setAttribute('stroke-width', '0.015');
solderGroup.appendChild(circle);
}
}
});
svgEl.appendChild(solderGroup);
}
function toggleSolder() {
solderVisible = !solderVisible;
if (solderGroup) solderGroup.style.display = solderVisible ? '' : 'none';
document.getElementById('solder-btn').classList.toggle('active', solderVisible);
}
// ── Label visibility toggle ──
let labelsVisible = false;
let labelGroups = [];
function setupLabelToggle() {
if (!svgEl) return;
// KiCad SVGs render labels as <g> containing <text> + <g class="stroked-text">
// Find all <text> elements and collect their parent <g> groups
svgEl.querySelectorAll('text').forEach(txt => {
const parent = txt.parentElement;
if (parent && parent.tagName === 'g' && !labelGroups.includes(parent)) {
labelGroups.push(parent);
parent.style.display = 'none'; // hidden by default
}
});
}
function toggleLabels() {
labelsVisible = !labelsVisible;
labelGroups.forEach(g => g.style.display = labelsVisible ? '' : 'none');
document.getElementById('labels-btn').classList.toggle('active', labelsVisible);
}
// ── Regen prompt modal ──
let _regenPrompt = '';
function buildRegenPrompt() {
const name = _partName || _fpName;
const folder = _kicadModPath.replace(/\/[^/]+$/, '');
return 'Regenerate the FpView for ' + name + '.\n\n'
+ 'Footprint: ' + _kicadModPath + '\n'
+ 'Metadata: ' + folder + '/\n\n'
+ 'Re-export the SVG via the KiCad service, regenerate the FpView HTML at the same path, '
+ 'and re-display it.';
}
function showRegenPrompt() {
_regenPrompt = buildRegenPrompt();
document.getElementById('regen-prompt-text').textContent = _regenPrompt;
document.getElementById('regen-overlay').classList.add('visible');
const status = document.getElementById('regen-copy-status');
navigator.clipboard.writeText(_regenPrompt).then(() => {
status.textContent = 'Copied to clipboard';
status.classList.add('copied');
}).catch(() => {
status.textContent = 'Click Copy to copy';
status.classList.remove('copied');
});
}
function closeRegenPrompt() {
document.getElementById('regen-overlay').classList.remove('visible');
}
function copyRegenText() {
navigator.clipboard.writeText(_regenPrompt).then(() => {
const status = document.getElementById('regen-copy-status');
status.textContent = 'Copied to clipboard';
status.classList.add('copied');
});
}
wrap.addEventListener('wheel', (e) => {
e.preventDefault();
const r = wrap.getBoundingClientRect();
zoomAt(e.clientX - r.left, e.clientY - r.top, e.deltaY < 0 ? 1.12 : 0.89);
}, { passive: false });
wrap.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
isDragging = true;
dragStartX = e.clientX; dragStartY = e.clientY;
panStartX = panX; panStartY = panY;
wrap.classList.add('dragging');
});
window.addEventListener('mousemove', (e) => {
const r = wrap.getBoundingClientRect();
// Convert screen coords to kicad_mod coords
const svgX = (e.clientX - r.left - panX) / scale / MM_TO_PX;
const svgY = (e.clientY - r.top - panY) / scale / MM_TO_PX;
const kicadX = svgX - offsetX;
const kicadY = svgY - offsetY;
cursorPos.innerHTML = 'X: ' + kicadX.toFixed(2) + ' Y: ' + kicadY.toFixed(2) + ' mm';
if (!isDragging) return;
panX = panStartX + (e.clientX - dragStartX);
panY = panStartY + (e.clientY - dragStartY);
updateTransform();
});
window.addEventListener('mouseup', () => { isDragging = false; wrap.classList.remove('dragging'); });
// ── Pad hit zones ──
let highlightedRects = [];
let stickyPad = null;
function setupPadHitZones() {
if (!svgEl) return;
pads.forEach(pad => {
const cx = pad.x + offsetX;
const cy = pad.y + offsetY;
const rx = cx - pad.width / 2;
const ry = cy - pad.height / 2;
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', rx);
rect.setAttribute('y', ry);
rect.setAttribute('width', pad.width);
rect.setAttribute('height', pad.height);
rect.setAttribute('fill', 'transparent');
rect.setAttribute('stroke', 'none');
rect.setAttribute('data-pad', pad.number);
rect.style.cursor = 'pointer';
rect.style.pointerEvents = 'all';
svgEl.appendChild(rect);
rect.addEventListener('mouseenter', (ev) => showPadTooltip(ev.clientX, ev.clientY, pad, rect));
rect.addEventListener('mousemove', (ev) => showPadTooltip(ev.clientX, ev.clientY, pad, rect));
rect.addEventListener('mouseleave', () => hidePadTooltip(rect));
rect.addEventListener('click', (ev) => { ev.stopPropagation(); handlePadClick(pad, rect); });
});
}
function buildTooltipHtml(pad) {
let html = '<div><span class="tt-pad">Pad ' + pad.number + '</span>';
if (pad.signal) html += '<span class="tt-signal">' + pad.signal + '</span>';
html += '</div>';
html += '<div><span class="tt-dims">' + pad.width.toFixed(3) + ' × ' + pad.height.toFixed(3) + ' mm · ' + pad.shape + ' · ' + pad.mountType + '</span></div>';
if (pad.desc) {
html += '<div class="tt-desc">' + pad.desc + '</div>';
}
if (datasheetUrl) {
html += '<div class="tt-source"><span class="tt-source-label">Source:</span> <a href="' + datasheetUrl + '" target="_blank">Datasheet</a></div>';
}
html += '<div class="tt-hint">click to pin · select text</div>';
return html;
}
function positionTooltip(x, y) {
// Render first to measure
tooltip.style.left = '0px';
tooltip.style.top = '0px';
const ttRect = tooltip.getBoundingClientRect();
const ttW = ttRect.width, ttH = ttRect.height;
const pad = 14;
let tx = x + 14, ty = y + 14;
if (tx + ttW > window.innerWidth - pad) tx = x - ttW - 14;
if (ty + ttH > window.innerHeight - pad) ty = y - ttH - 14;
tx = Math.max(pad, Math.min(tx, window.innerWidth - ttW - pad));
ty = Math.max(pad, Math.min(ty, window.innerHeight - ttH - pad));
tooltip.style.left = tx + 'px';
tooltip.style.top = ty + 'px';
}
/* lbr-hover-bus v1 */ window.addEventListener('message',function(e){var d=e.data||{};if(d.type!=='lbr-hover')return;clearHighlight();if(d.pin==null||!svgEl)return;svgEl.querySelectorAll('rect[data-pad]').forEach(function(r){if(r.getAttribute('data-pad')===String(d.pin)){r.setAttribute('stroke','#00b8b1');r.setAttribute('stroke-width','0.12');r.setAttribute('fill','rgba(0,184,177,0.2)');highlightedRects.push(r);}});});
function showPadTooltip(x, y, pad, rect) {
try{parent.postMessage({type:'lbr-hover',pin:String(pad.number)},'*')}catch(_){}
if (stickyPad !== null) return; // suppress hover while sticky
clearHighlight();
rect.setAttribute('stroke', '#00b8b1');
rect.setAttribute('stroke-width', '0.12');
rect.setAttribute('fill', 'rgba(0,184,177,0.2)');
highlightedRects.push(rect);
tooltip.innerHTML = buildTooltipHtml(pad);
tooltip.classList.add('visible');
tooltip.classList.remove('sticky');
positionTooltip(x, y);
}
function hidePadTooltip(rect) { try{parent.postMessage({type:'lbr-hover',pin:null},'*')}catch(_){}
if (stickyPad !== null) return; // don't hide while sticky
tooltip.classList.remove('visible');
clearHighlight();
}
function handlePadClick(pad, rect) {
if (stickyPad === pad.number) {
// Unpin
stickyPad = null;
tooltip.classList.remove('sticky', 'visible');
clearHighlight();
} else {
// Pin this pad
stickyPad = pad.number;
clearHighlight();
rect.setAttribute('stroke', '#00b8b1');
rect.setAttribute('stroke-width', '0.12');
rect.setAttribute('fill', 'rgba(0,184,177,0.2)');
highlightedRects.push(rect);
tooltip.innerHTML = buildTooltipHtml(pad);
tooltip.classList.add('visible', 'sticky');
// Remove hint when sticky
const hint = tooltip.querySelector('.tt-hint');
if (hint) hint.style.display = 'none';
}
}
// Dismiss sticky tooltip on click outside
document.addEventListener('click', (e) => {
if (stickyPad !== null && !tooltip.contains(e.target)) {
stickyPad = null;
tooltip.classList.remove('sticky', 'visible');
clearHighlight();
}
});
function clearHighlight() {
highlightedRects.forEach(r => {
r.setAttribute('fill', 'transparent');
r.setAttribute('stroke', 'none');
});
highlightedRects = [];
}
wrap.addEventListener('mouseleave', () => {
if (stickyPad !== null) return;
tooltip.classList.remove('visible');
clearHighlight();
});
// Keyboard
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeRegenPrompt();
else if (e.key === 'f' || e.key === 'F') zoomFit();
else if (e.key === '1') zoomReset();
else if (e.key === '+' || e.key === '=') zoomIn();
else if (e.key === '-') zoomOut();
else if (e.key === 'g' || e.key === 'G') toggleGrid();
else if (e.key === 's' || e.key === 'S') toggleSolder();
else if (e.key === 'l' || e.key === 'L') toggleLabels();
});
// Screenshot capture via postMessage (parent viewer sends mgmt_capture_request)
window.addEventListener('message', (e) => {
if (e.data?.type === 'mgmt_capture_request' && svgEl) {
const xml = new XMLSerializer().serializeToString(svgEl);
parent.postMessage({
type: 'mgmt_canvas_capture',
_reqId: e.data._reqId,
svgXml: xml,
svgWidth: svgW,
svgHeight: svgH,
bgColor: 'undefined',
}, '*');
}
});
// Auto-fit on window/container resize
new ResizeObserver(() => zoomFit()).observe(wrap);
window.addEventListener('resize', () => requestAnimationFrame(zoomFit));
// Init — set up layers then fit. Use multiple retries because srcdoc iframes
// may not have layout dimensions during the first requestAnimationFrame.
let inited = false;
function init() {
if (inited) return;
setupLabelToggle();
setupSolderBlobs();
setupPadHitZones();
inited = true;
}
function initAndFit() {
init();
zoomFit();
}
requestAnimationFrame(initAndFit);
setTimeout(initAndFit, 100);
setTimeout(initAndFit, 500);
</script>
</body>
</html>