/* * Copyright (c) 2025-2026 Taras Greben * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace * See LICENSE file for details. */ // ── UI helpers ──────────────────────────────────────────────── import { db, uuid } from './db.js'; import { S, getNetColor } from './state.js'; import { compColor, compLabel } from './components.js'; import { render } from './draw.js'; import { pushHistory, undo } from './history.js'; import { fitView, routeWires } from './layout.js'; import { importSelectedFromZip, importSymbolsFromText } from './kicad.js'; // ── Toast notification ──────────────────────────────────────── let _toastTimer; export function toast(msg, type = '') { const el = document.getElementById('toast'); el.textContent = msg; el.className = 'show' + (type ? ' ' + type : ''); clearTimeout(_toastTimer); _toastTimer = setTimeout(() => { el.className = type || ''; }, 2600); } // ── Status bar ──────────────────────────────────────────────── export function setStatus(msg, type = '', shortMsg = null) { const el = document.getElementById('status-msg'); el.className = type; if (shortMsg) { el.setAttribute('data-long', msg); el.setAttribute('data-short', shortMsg); el.classList.add('responsive-text'); el.textContent = ''; } else { el.removeAttribute('data-long'); el.removeAttribute('data-short'); el.classList.remove('responsive-text'); el.textContent = msg; } } // ── Modal helpers ───────────────────────────────────────────── export function showModal(id) { document.getElementById(id).classList.add('active'); } export function hideModal(id) { document.getElementById(id).classList.remove('active'); } // ── Properties panel ───────────────────────────────────────── export function showProperties(comp) { document.getElementById('right-panel')?.classList.add('open'); const col = compColor(comp.type); let extraProps = ''; if (comp.type !== 'KICAD') { extraProps = `
Pin Count
`; } const isKicad = comp.type === 'KICAD' && comp.kicadData?.pins; const kPins = isKicad ? comp.kicadData.pins : new Array(); let shiftUi = `
Pins (${comp.pins.length})
`; if (isKicad && kPins.length > 0) { let shiftVal = '0'; if (comp.overrides?.type === 'shift') shiftVal = comp.overrides.offset.toString(); else if (comp.overrides?.type === 'remap') shiftVal = 'custom'; let shiftOptions = ``; for (let i = 0; i < kPins.length; i++) { shiftOptions += ``; } shiftUi = `
Pins (${comp.pins.length})
Shift
`; } document.getElementById('prop-panel').innerHTML = `
Ref
${comp.ref}
Type
${compLabel(comp.type)}
Value
${extraProps}
${shiftUi} ${comp.pins.map((p, i) => { let mappingUi = ''; if (isKicad) { const options = kPins.map(kp => `` ).join(''); mappingUi = `
${p.originalName || p.name}
`; } else { const mappingStr = p.name !== (p.originalName || p.name) ? ` →${p.name}` : ''; mappingUi = `${p.originalName || p.name}${mappingStr}`; } return `
${mappingUi} ${p.net || '—'}
`; }).join('')}
Right-click / long-press to rotate or replace.
`; // ── Sync Shift Dropdown and Individual Pin Selects ── if (isKicad && kPins.length > 0) { const shiftSelect = document.getElementById('kicad-pin-shift'); const pinSelects = Array.from(document.querySelectorAll('.kicad-pin-map')); const checkShiftState = () => { let detectedOffset = -1; for (let offset = 0; offset < kPins.length; offset++) { let isMatch = true; for (let i = 0; i < comp.pins.length; i++) { const mappedNum = pinSelects[i].value; const expectedNum = kPins[(i + offset) % kPins.length]?.num; if (mappedNum !== expectedNum) { isMatch = false; break; } } if (isMatch) { detectedOffset = offset; break; } } shiftSelect.value = detectedOffset >= 0 ? detectedOffset.toString() : 'custom'; }; shiftSelect.addEventListener('change', () => { if (shiftSelect.value === 'custom') return; const offset = parseInt(shiftSelect.value); pinSelects.forEach((sel, i) => { const expectedNum = kPins[(i + offset) % kPins.length]?.num; sel.value = expectedNum; }); }); pinSelects.forEach(sel => sel.addEventListener('change', checkShiftState)); } // Dynamically fetch and render extra DB properties for KiCad symbols if (comp.componentTypeId) { db.getResolvedKicadDataForComponentType(comp.componentTypeId).then(kSym => { if (!kSym) return; const container = document.getElementById('kicad-extra-props'); if (!container) return; // Panel might have been closed/switched let html = ''; if (kSym.description) { html += `
${kSym.description}
`; } const footprint = kSym.footprint || kSym.fp_filters; if (footprint) { html += `
FP: ${footprint}
`; } if (kSym.datasheet && kSym.datasheet !== '~') { html += `
View Datasheet
`; } container.innerHTML = html; }); } document.getElementById('pa').onclick = async () => { comp.value = document.getElementById('pv').value; if (comp.type === 'KICAD') { const selects = document.querySelectorAll('.kicad-pin-map'); if (selects.length > 0) { const newMap = {}; selects.forEach(sel => { newMap[sel.getAttribute('data-orig')] = sel.value; }); let detectedOffset = -1; // Detect if the mapping is a uniform CCW shift for (let offset = 0; offset < kPins.length; offset++) { let isMatch = true; for (let i = 0; i < comp.pins.length; i++) { const orig = comp.pins[i].originalName; const mappedNum = newMap[orig]; const expectedNum = kPins[(i + offset) % kPins.length]?.num; if (mappedNum !== expectedNum) { isMatch = false; break; } } if (isMatch) { detectedOffset = offset; break; } } if (detectedOffset === 0) { comp.overrides = null; // Identity mapping } else if (detectedOffset > 0) { comp.overrides = { type: 'shift', offset: detectedOffset }; } else { // Fallback identity check just in case let isIdentity = true; for (let i = 0; i < comp.pins.length; i++) { if (newMap[comp.pins[i].originalName] !== kPins[i]?.num) { isIdentity = false; break; } } if (isIdentity) comp.overrides = null; else comp.overrides = { type: 'remap', map: newMap }; } } const m = await import('./app.js'); m.applyPinOverrides(comp); const l = await import('./layout.js'); await l.buildAndRoute({}, false); m.saveComponentLayout(comp.id); } else { const newCount = parseInt(document.getElementById('ppc').value) || comp.pins.length; const minCount = Math.max(1, comp.pins.filter(p => p.net).length); const safeCount = Math.max(minCount, newCount); if (safeCount > comp.pins.length) { let nextPin = 1; while (comp.pins.length < safeCount) { if (!comp.pins.find(p => p.originalName === String(nextPin))) { comp.pins.push({ name: String(nextPin), originalName: String(nextPin), net: null }); } nextPin++; } } else if (safeCount < comp.pins.length) { const keptPins = new Array(); comp.pins.forEach(p => { if (p.net || keptPins.length < safeCount) keptPins.push(p); }); comp.pins = keptPins; } const l = await import('./layout.js'); await l.buildAndRoute({}, false); const m = await import('./app.js'); m.saveComponentLayout(comp.id); } updateSidePanels(); showProperties(comp); toast('Updated ' + comp.ref); }; } export function showNetProperties(net) { document.getElementById('right-panel')?.classList.add('open'); const col = net.color || getNetColor(net.name); const rows = (net.nodes || new Array()).map(nd => { const comp = S.components.find(c => c.ref === nd.ref); const cCol = comp ? compColor(comp.type) : '#94a3b8'; return `
${nd.ref} pin ${nd.pin}
`; }).join(''); document.getElementById('prop-panel').innerHTML = `
Net
${net.name}
Connections (${(net.nodes || new Array()).length})
${rows || '
No connections
'}
`; } export function showDefaultProps() { document.getElementById('right-panel')?.classList.remove('open'); document.getElementById('prop-panel').innerHTML = '
' + 'Click a component or wire to select.

' + 'Drag component to move.
Right-click for rotation.
'; } // ── Side panels ─────────────────────────────────────────────── export function updateSidePanels() { // Net list const nl = document.getElementById('net-list'); nl.innerHTML = ''; S.nets.forEach(net => { const d = document.createElement('div'); d.className = 'net-item' + (net.isWip ? ' wip' : '') + (S.selectedNet === net.name ? ' selected' : ''); d.innerHTML = ` ${net.name} ${net.nodes.length}⬥`; d.onclick = () => { if (window.innerWidth <= 800) document.getElementById('left-panel')?.classList.remove('open'); S.selectedNet = S.selectedNet === net.name ? null : net.name; S.selectedComp = null; S.selectedWire = null; if (S.selectedNet) showNetProperties(net); else showDefaultProps(); updateSidePanels(); render(); }; nl.appendChild(d); }); // Component list const cl = document.getElementById('comp-list'); cl.innerHTML = ''; S.components.forEach(c => { const d = document.createElement('div'); d.className = 'comp-item' + (S.selectedComp === c.id ? ' selected' : ''); d.innerHTML = ` ${c.ref} ${c.value}`; d.onclick = () => { if (window.innerWidth <= 800) document.getElementById('left-panel')?.classList.remove('open'); S.selectedComp = c.id; S.selectedWire = null; S.selectedNet = null; showProperties(c); updateSidePanels(); render(); }; cl.appendChild(d); }); } // ── Export modal ─────────────────────────────────────────────── let _expFile = '', _expContent = ''; export function showExport(title, content, fname) { _expFile = fname; _expContent = content; document.getElementById('export-title').textContent = title; document.getElementById('export-content').textContent = content; showModal('modal-export'); } // ── Button wiring (called once from app.js) ─────────────────── export function initUI(mode, importNetlistStandalone, resetLayout, hasLayoutData) { const isEmbedded = mode === 'embedded'; const btnLeft = document.getElementById('btn-toggle-left'); if (btnLeft) btnLeft.onclick = () => document.getElementById('left-panel').classList.toggle('open'); const btnRight = document.getElementById('btn-toggle-right'); if (btnRight) btnRight.onclick = () => document.getElementById('right-panel').classList.toggle('open'); const hideEl = id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; }; // In embedded mode, hide standalone-only controls if (isEmbedded) { hideEl('btn-load-db'); hideEl('btn-import-file'); hideEl('es-load-btn'); } initLibraryManager(); // Load-from-DB button — repurposed as a hint in standalone mode const loadBtn = document.getElementById('btn-load-db'); if (loadBtn && !isEmbedded) { loadBtn.onclick = () => { localStorage.setItem('pcb_startup_tab', 'schema'); window.location.href = 'studio.html'; } } // Empty-state button — opens import modal in standalone mode const esBtn = document.getElementById('es-load-btn'); if (esBtn && !isEmbedded) { esBtn.onclick = () => { document.getElementById('import-textarea').value = ''; document.getElementById('file-name').textContent = 'No file'; showModal('modal-import'); }; } // Import button in header const importBtn = document.getElementById('btn-import-file'); if (importBtn && !isEmbedded) { importBtn.onclick = () => { document.getElementById('import-textarea').value = ''; document.getElementById('file-name').textContent = 'No file'; showModal('modal-import'); }; } document.getElementById('btn-reroute').onclick = async () => { if (!S.hasData) { toast('No netlist', 'warn'); return; } pushHistory(); const l = await import('./layout.js'); await l.buildAndRoute({}, false); toast('Re-routed schematic diagram'); }; document.getElementById('btn-auto-place').onclick = async () => { if (!S.hasData) { toast('No netlist', 'warn'); return; } if (hasLayoutData && hasLayoutData()) { // Use Studio's confirmAction if available (embedded), else use confirm() const message = 'Reset layout? All manually placed positions will be lost.'; const ok = window.parent?.confirmAction ? await window.parent.confirmAction(message, 'Reset Layout') : window.confirm(message); if (!ok) return; } pushHistory(); if (resetLayout) await resetLayout(); toast('Auto layout applied'); }; // fitView already calls render() internally document.getElementById('btn-zoom-fit').onclick = fitView; document.getElementById('btn-zoom-fit2').onclick = fitView; document.getElementById('btn-zoom-in').onclick = () => { S.zoom = Math.min(8, S.zoom * 1.25); render(); }; document.getElementById('btn-zoom-out').onclick = () => { S.zoom = Math.max(0.1, S.zoom / 1.25); render(); }; document.getElementById('btn-undo').onclick = undo; // Import modal — file picker and OK const pickBtn = document.getElementById('btn-pick-file'); if (pickBtn) pickBtn.onclick = () => document.getElementById('file-input').click(); document.getElementById('file-input').onchange = e => { const f = e.target.files[0]; if (!f) return; document.getElementById('file-name').textContent = f.name; const r = new FileReader(); r.onload = ev => document.getElementById('import-textarea').value = ev.target.result; r.readAsText(f); }; document.getElementById('btn-import-cancel').onclick = () => hideModal('modal-import'); document.getElementById('btn-import-ok').onclick = () => { const text = document.getElementById('import-textarea').value.trim(); const fname = document.getElementById('file-name').textContent; if (!text) { toast('Nothing to import', 'err'); return; } hideModal('modal-import'); if (importNetlistStandalone) { importNetlistStandalone(text, fname !== 'No file' ? fname : 'Imported Netlist'); } }; // Export buttons document.getElementById('btn-export-kicad').onclick = () => { if (!S.hasData) { toast('No schematic', 'warn'); return; } import('./io.js').then(async m => showExport('KiCad Schematic (.kicad_sch)', await m.exportKiCad(), await m.getExportFilename('kicad_sch'))); }; /* document.getElementById('btn-export-spice').onclick = () => { if (!S.hasData) { toast('No schematic', 'warn'); return; } import('./io.js').then(async m => showExport('SPICE (.cir) — LTspice/ngspice/KiCad Sim/MicroCAP', m.exportSpice(), await m.getExportFilename('cir'))); }; */ // Export modal actions document.getElementById('btn-export-close').onclick = () => hideModal('modal-export'); document.getElementById('btn-export-copy').onclick = () => navigator.clipboard.writeText(_expContent).then(() => toast('Copied')); document.getElementById('btn-export-download').onclick = () => { const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([_expContent], { type: 'text/plain' })); a.download = _expFile; a.click(); toast('Downloaded ' + _expFile); }; // Close modal on backdrop click document.querySelectorAll('.modal-overlay').forEach(o => o.addEventListener('click', e => { if (e.target === o) o.classList.remove('active'); }) ); } // ── Library Manager Modal ────────────────────────────────────── function initLibraryManager() { const modal = document.getElementById('modal-library-manager'); const btnOpen = document.getElementById('btn-lib-mgr'); const defaultUi = document.getElementById('lib-default-ui'); const zipUi = document.getElementById('lib-zip-explorer'); const treeEl = document.getElementById('zip-tree'); const logEl = document.getElementById('lib-import-log'); let currentZip = null; let libsData = {}; const log = (msg) => { logEl.innerHTML += `
${msg}
`; logEl.scrollTop = logEl.scrollHeight; }; const resetZipUi = () => { if (currentZip?.reader) currentZip.reader.close().catch(() => {}); currentZip = null; libsData = {}; defaultUi.style.display = 'flex'; zipUi.style.display = 'none'; }; if (btnOpen) btnOpen.onclick = () => { modal.classList.add('active'); resetZipUi(); }; document.getElementById('btn-lib-close').onclick = () => { modal.classList.remove('active'); logEl.innerHTML = ''; resetZipUi(); }; document.getElementById('btn-zip-cancel').onclick = resetZipUi; document.getElementById('btn-zip-sel-all').onclick = () => document.querySelectorAll('.cb-lib').forEach(cb => { cb.checked = true; cb.dispatchEvent(new Event('change')); }); document.getElementById('btn-zip-sel-none').onclick = () => document.querySelectorAll('.cb-lib').forEach(cb => { cb.checked = false; cb.dispatchEvent(new Event('change')); }); async function processZipBuffer(buffer) { log('Parsing ZIP directory structure...'); const { ZipReader, Uint8ArrayReader } = zip; const reader = new ZipReader(new Uint8ArrayReader(new Uint8Array(buffer)), { // useWebWorkers: false, useCompressionStream: true }); const entries = await reader.getEntries(); // Keep reader open — needed later when user clicks Import currentZip = { reader, entries }; libsData = {}; entries .filter(e => !e.directory && e.filename.endsWith('.kicad_sym')) .forEach(e => { const match = e.filename.match(/([^\/]+)\.kicad_symdir\//); const libName = match ? match[1] : 'Imported'; if (!libsData[libName]) libsData[libName] = []; libsData[libName].push({ path: e.filename, name: e.filename.split('/').pop().replace('.kicad_sym', '') }); }); treeEl.innerHTML = ''; Object.keys(libsData).sort().forEach(libName => { const libDiv = document.createElement('div'); libDiv.style.marginBottom = '6px'; const head = document.createElement('div'); head.style.display = 'flex'; head.style.alignItems = 'center'; head.style.gap = '6px'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.className = 'cb-lib'; cb.dataset.lib = libName; const lbl = document.createElement('span'); lbl.textContent = `${libName} (${libsData[libName].length} symbols)`; lbl.style.flex = '1'; lbl.style.cursor = 'pointer'; lbl.onclick = () => { cb.checked = !cb.checked; cb.dispatchEvent(new Event('change')); }; const btnExp = document.createElement('button'); btnExp.textContent = '▼'; btnExp.style.background = 'none'; btnExp.style.border = 'none'; btnExp.style.color = 'var(--text2)'; btnExp.style.cursor = 'pointer'; head.appendChild(cb); head.appendChild(lbl); head.appendChild(btnExp); libDiv.appendChild(head); const symsDiv = document.createElement('div'); symsDiv.style.display = 'none'; symsDiv.style.paddingLeft = '20px'; symsDiv.style.flexDirection = 'column'; symsDiv.style.marginTop = '4px'; let rendered = false; btnExp.onclick = () => { if (symsDiv.style.display === 'none') { symsDiv.style.display = 'flex'; btnExp.textContent = '▲'; if (!rendered) { libsData[libName].forEach(sym => { const sRow = document.createElement('label'); sRow.style.display = 'flex'; sRow.style.gap = '6px'; sRow.style.fontSize = '10px'; sRow.style.color = 'var(--text)'; const sCb = document.createElement('input'); sCb.type = 'checkbox'; sCb.className = `cb-sym cb-sym-${libName}`; sCb.dataset.path = sym.path; sCb.checked = cb.checked; sCb.onchange = () => { const allSyms = Array.from(document.querySelectorAll(`.cb-sym-${libName}`)); const checkedCount = allSyms.filter(x => x.checked).length; cb.checked = checkedCount === allSyms.length; cb.indeterminate = checkedCount > 0 && checkedCount < allSyms.length; }; sRow.appendChild(sCb); sRow.appendChild(document.createTextNode(sym.name)); symsDiv.appendChild(sRow); }); rendered = true; } } else { symsDiv.style.display = 'none'; btnExp.textContent = '▼'; } }; cb.onchange = () => { cb.indeterminate = false; if (rendered) document.querySelectorAll(`.cb-sym-${libName}`).forEach(x => x.checked = cb.checked); }; libDiv.appendChild(symsDiv); treeEl.appendChild(libDiv); }); defaultUi.style.display = 'none'; zipUi.style.display = 'flex'; } document.getElementById('btn-zip-import').onclick = async () => { const selectedPaths = new Array(); document.querySelectorAll('.cb-lib').forEach(cbLib => { const libName = cbLib.dataset.lib; const symsDiv = cbLib.parentElement.nextElementSibling; const rendered = symsDiv.children.length > 0; if (rendered) { document.querySelectorAll(`.cb-sym-${libName}:checked`).forEach(sCb => selectedPaths.push(sCb.dataset.path)); } else if (cbLib.checked) { libsData[libName].forEach(sym => selectedPaths.push(sym.path)); } }); if (selectedPaths.length === 0) return toast('No files selected', 'warn'); // Detach without closing — importSelectedFromZip owns the reader now and will close it const zipRef = currentZip; currentZip = null; libsData = {}; defaultUi.style.display = 'flex'; zipUi.style.display = 'none'; log(`Extracting and parsing ${selectedPaths.length} files...`); try { const stats = await importSelectedFromZip(zipRef, selectedPaths, (msg) => { // Update last line if it looks like a progress message, otherwise append const last = logEl.lastElementChild; if (last && last.dataset.progress) { last.textContent = msg; } else { const div = document.createElement('div'); div.dataset.progress = '1'; div.textContent = msg; logEl.appendChild(div); } logEl.scrollTop = logEl.scrollHeight; }); log(`Success: ${stats.inserted} inserted, ${stats.updated} updated.`); toast('Import Complete', 'ok'); } catch (err) { log(`Error: ${err.message}`); } }; document.getElementById('btn-lib-import-url').onclick = async () => { const url = document.getElementById('lib-remote-url').value.trim(); if (!url) return toast('Please enter a URL', 'warn'); log(`Fetching ${url}...`); try { const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); if (url.endsWith('.zip')) { const buf = await res.arrayBuffer(); await processZipBuffer(buf); } else { const text = await res.text(); const libName = url.split('/').pop().replace('.kicad_sym', ''); const stats = await importSymbolsFromText(text, libName); log(`Success: ${stats.inserted} inserted, ${stats.updated} updated into '${libName}'.`); toast('Library Imported Successfully', 'ok'); } } catch (err) { log(`Error: ${err.message}`); } }; const fileInput = document.getElementById('lib-file-input'); document.getElementById('btn-lib-pick-file').onclick = () => fileInput.click(); fileInput.onchange = async (e) => { const f = e.target.files[0]; if (!f) return; document.getElementById('lib-file-name').textContent = f.name; log(`Reading local file: ${f.name}...`); try { if (f.name.endsWith('.zip')) { const buf = await f.arrayBuffer(); await processZipBuffer(buf); } else { const text = await f.text(); const libName = f.name.replace('.kicad_sym', ''); const stats = await importSymbolsFromText(text, libName); log(`Success: ${stats.inserted} inserted, ${stats.updated} updated into '${libName}'.`); toast('Library Imported Successfully', 'ok'); } } catch (err) { log(`Error: ${err.message}`); } fileInput.value = ''; }; } // ── Replace Symbol Modal ────────────────────────────────────── export async function showReplaceSymbolModal(comp) { const modal = document.getElementById('modal-replace-symbol'); const listEl = document.getElementById('sym-list'); const searchEl = document.getElementById('sym-search'); listEl.innerHTML = '
Loading Library...
'; modal.classList.add('active'); const t0 = performance.now(); const targetPinCount = comp.pins.length; // Instantly fetch ONLY the components that match the required pin count using the DB index const matchingSyms = await db.getKicadSymbolsByPinCount(targetPinCount); const t1 = performance.now(); // console.log(`[Perf] DB Fetch/Cache: ${matchingSyms.length} records in ${(t1 - t0).toFixed(1)}ms`); // Parse JSON only for the few thousand that actually matched pin count const validSyms = matchingSyms.map(s => ({ ...s, parsed: JSON.parse(s.parsedData) })); const t2 = performance.now(); // console.log(`[Perf] Fast Filter & Selective Parse: left ${validSyms.length} records in ${(t2 - t1).toFixed(1)}ms`); const renderList = (filterText = '') => { listEl.innerHTML = ''; // 1. Parse the search string const terms = filterText.toLowerCase().split(/\s+/).filter(Boolean); const filters = { name: null, lib: null, ref: null, desc: null, pins: null, text: new Array() }; terms.forEach(term => { if (term.startsWith('name:')) filters.name = term.slice(5); else if (term.startsWith('lib:')) filters.lib = term.slice(4); else if (term.startsWith('ref:')) filters.ref = term.slice(4); else if (term.startsWith('desc:')) filters.desc = term.slice(5); else filters.text.push(term); }); // 2. Filter the symbols const filtered = validSyms.filter(s => { if (filters.name && !s.symbol.toLowerCase().includes(filters.name)) return false; if (filters.lib && !s.library.toLowerCase().includes(filters.lib)) return false; if (filters.ref && !(s.reference || '').toLowerCase().includes(filters.ref)) return false; if (filters.desc && !(s.description || '').toLowerCase().includes(filters.desc)) return false; if (filters.text.length > 0) { const combinedText =[ s.symbol, s.library, s.description || '', s.keywords || '', s.fp_filters || '' ].join(' ').toLowerCase(); // All general text terms must match somewhere in the combined text if (!filters.text.every(t => combinedText.includes(t))) return false; } return true; }); // 3. Render the list if (filtered.length === 0) { listEl.innerHTML = '
No matching symbols found.
'; return; } filtered.forEach(s => { const item = document.createElement('div'); item.className = 'db-item'; const symNameHtml = s.datasheet && s.datasheet !== '~' ? `${s.symbol}` : `${s.symbol}`; item.innerHTML = `
${s.library} ${symNameHtml}
${s.description || 'No description'}
Ref: ${s.reference || '?'}
${s.fp_filters ? `FP: ${s.fp_filters.substring(0, 15)}...
` : ''} ${s.parsed.pins.length} pins
`; item.onclick = async () => { modal.classList.remove('active'); await applyKiCadSymbolReplacement(comp, s); }; listEl.appendChild(item); }); }; // Setup smart default search string let defaultSearch = ''; if (['R','C','L','D','Z'].includes(comp.type)) { defaultSearch = `lib:device ref:${comp.type}`; } else { const refMatch = comp.ref.match(/^[A-Za-z]+/); defaultSearch = comp.value ? comp.value : (refMatch ? `ref:${refMatch[0]}` : ''); } searchEl.value = defaultSearch; renderList(defaultSearch); searchEl.oninput = (e) => renderList(e.target.value); document.getElementById('btn-replace-cancel').onclick = () => modal.classList.remove('active'); } async function applyKiCadSymbolReplacement(comp, kicadSym) { let ct = await db.getComponentTypeForKicadSymbol(kicadSym.id); if (!ct) { const newCtId = uuid(); await db.saveComponentType({ id: newCtId, symbolTypeId: null, name: kicadSym.symbol, pinCount: kicadSym.parsed.pins.length, created: Date.now(), lastModified: Date.now() }); await db.saveComponentTypeKicadSymbol({ id: uuid(), componentTypeId: newCtId, kicadSymbolId: kicadSym.id, isPrimary: 1 }); ct = { id: newCtId }; } const pinMap = {}; comp.pins.forEach((p, i) => { if (kicadSym.parsed.pins[i]) p.name = kicadSym.parsed.pins[i].num; pinMap[i + 1] = i + 1; // 1:1 mapping mapping for existing pins based on count }); comp.type = 'KICAD'; comp.componentTypeId = ct.id; comp.kicadData = { name: kicadSym.symbol, ...kicadSym.parsed }; // Because we replaced the symbol, use the optimized replacement route import('./layout.js').then(l => l.replaceComponentRoute(comp, pinMap)).then(() => { import('./app.js').then(({ saveComponentLayout }) => saveComponentLayout(comp.id)); }); toast(`Replaced with: ${kicadSym.symbol}`); }