// ── Exports ─────────────────────────────────────────────────── import { S } from './state.js'; import { createComp } from './components.js'; import { db, uuid } from './db.js'; function toSexpr(node) { if (Array.isArray(node)) { return '(' + node.map(toSexpr).join(' ') + ')'; } if (typeof node === 'string') { if (node === '' || /[^\w\-.]/.test(node)) return `"${node.replace(/"/g, '\\"')}"`; return node; } return String(node); } export async function getExportFilename(ext) { const bId = window._boardId || new URLSearchParams(window.location.search).get('boardId'); const dId = window._deviceId || new URLSearchParams(window.location.search).get('deviceId'); let dName = "Device"; let bName = "Board"; try { if (bId) { const board = await db.getBoard(bId); if (board && board.name) bName = board.name.replace(/[^a-z0-9_\-]/gi, '_'); const fetchedDevId = dId || (board ? board.deviceId : null); if (fetchedDevId) { const device = await db.getDevice(fetchedDevId); if (device && device.name) dName = device.name.replace(/[^a-z0-9_\-]/gi, '_'); } } } catch(e) { console.warn("Failed to fetch filename info", e); } return `${dName}-${bName}.${ext}`; } // ── KiCad Schematic (.kicad_sch) ────────────────────────────── export async function exportKiCad() { const K_SCALE = 0.127; // 20px = 2.54mm -> 1px = 0.127mm const K_OFFSET_X = 148.5; // Center of A4 paper horizontally const K_OFFSET_Y = 105.0; // Center of A4 paper vertically let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; S.components.forEach(c => { minX = Math.min(minX, c.x); maxX = Math.max(maxX, c.x); minY = Math.min(minY, c.y); maxY = Math.max(maxY, c.y); }); if (minX === Infinity) { minX = 0; maxX = 0; minY = 0; maxY = 0; } const centerOffX = (minX + maxX) / 2; const centerOffY = (minY + maxY) / 2; const sheetUuid = uuid(); const bId = window._boardId || new URLSearchParams(window.location.search).get('boardId'); const dId = window._deviceId || new URLSearchParams(window.location.search).get('deviceId'); let title = "SchemaReTrace Export"; let comment1 = "Reverse-engineered using PCB ReTrace (pcb.etaras.com)"; try { if (bId) { const board = await db.getBoard(bId); if (board && board.name) title = board.name; const fetchedDevId = dId || (board ? board.deviceId : null); if (fetchedDevId) { const device = await db.getDevice(fetchedDevId); if (device && device.name) title = device.name + " - " + board.name; } } } catch(e) {} const dateStr = new Date().toISOString().split('T')[0]; let o = `(kicad_sch (version 20231120) (generator SchemaReTrace)\n`; o += ` (uuid "${sheetUuid}")\n (paper "A4")\n`; o += ` (title_block\n`; o += ` (title "${title}")\n`; o += ` (date "${dateStr}")\n`; o += ` (comment 1 "${comment1}")\n`; o += ` )\n`; o += ` (lib_symbols\n`; const getKiCadLibData = async (c) => { if (c.componentTypeId) { const kSym = await db.getResolvedKicadDataForComponentType(c.componentTypeId); if (kSym) { const parsed = JSON.parse(kSym.parsedData); return { libId: `${kSym.library}:${kSym.symbol}`, symName: kSym.symbol, graphics: parsed.graphics, pins: parsed.pins, props: parsed.props || {}, footprint: kSym.footprint || "", datasheet: kSym.datasheet || "~" }; } } if (c.kicadData) { return { libId: `${c.kicadData.library || 'Device'}:${c.kicadData.name}`, symName: c.kicadData.name, graphics: c.kicadData.graphics, pins: c.kicadData.pins, props: c.kicadData.props || {}, footprint: c.kicadData.footprint || "", datasheet: c.kicadData.datasheet || "~" }; } let symName = 'Unknown'; let libId = 'Device:Unknown'; switch(c.type) { case 'R': libId = 'Device:R'; symName = 'R'; break; case 'C': libId = 'Device:C'; symName = 'C'; break; case 'L': libId = 'Device:L'; symName = 'L'; break; case 'D': libId = 'Device:D'; symName = 'D'; break; case 'Z': libId = 'Device:D_Zener'; symName = 'D_Zener'; break; default: if (c.ref.startsWith('Q')) { libId = 'Device:Q_NPN_BCE'; symName = 'Q_NPN_BCE'; } else if (c.ref.startsWith('J')) { libId = 'Device:Q_NJFET_DGS'; symName = 'Q_NJFET_DGS'; } else { symName = createComp(c).label || 'Unknown'; libId = `Device:${symName}`; } } return { libId, symName, graphics: null, pins: null, props: {}, footprint: "", datasheet: "~" }; }; const usedLibs = new Map(); const compLibMap = new Map(); for (const c of S.components) { const libData = await getKiCadLibData(c); compLibMap.set(c.id, libData); if (usedLibs.has(libData.libId)) continue; let refPrefix = c.ref.match(/^[A-Za-z]+/)?.[0] || 'U'; const pfx = refPrefix.toUpperCase(); const isPassiveOrTransistor =['R', 'C', 'L', 'D', 'Z', 'Q', 'J', 'LED'].includes(pfx) ||['R', 'C', 'L', 'D', 'Z'].includes(pfx.charAt(0)); const hidePinsStr = isPassiveOrTransistor ? ` (pin_numbers hide) (pin_names hide)` : ''; let symDef = ` (symbol "${libData.libId}" (in_bom yes) (on_board yes)${hidePinsStr}\n`; // Map dynamically extracted library properties let propId = 0; const symProps = { "Reference": refPrefix, "Value": libData.symName, "Footprint": libData.footprint || "", "Datasheet": libData.datasheet || "~", ...(libData.props || {}) };['Reference', 'Value', 'Footprint', 'Datasheet'].forEach(k => { let hideStr = (k === 'Reference' || k === 'Value') ? '' : ' hide'; symDef += ` (property "${k}" "${String(symProps[k] || '').replace(/"/g, '\\"')}" (id ${propId++}) (at 0 0 0) (effects (font (size 1.27 1.27))${hideStr}))\n`; delete symProps[k]; }); for (const [k, v] of Object.entries(symProps)) { symDef += ` (property "${k}" "${String(v).replace(/"/g, '\\"')}" (id ${propId++}) (at 0 0 0) (effects (font (size 1.27 1.27)) hide))\n`; } if (libData.graphics && libData.pins) { symDef += ` (symbol "${libData.symName}_0_1"\n`; libData.graphics.forEach(g => { symDef += ` ${toSexpr(g)}\n`; }); symDef += ` )\n`; symDef += ` (symbol "${libData.symName}_1_1"\n`; libData.pins.forEach(p => { const px = p.x * K_SCALE; const py = -p.y * K_SCALE; const plen = (p.len || 20) * K_SCALE; // Output the exact electrical type and style parsed from the DB const eType = p.electrical_type || 'passive'; const gStyle = p.graphical_style || 'line'; symDef += ` (pin ${eType} ${gStyle} (at ${px.toFixed(3)} ${py.toFixed(3)} ${p.angle || 0}) (length ${plen.toFixed(3)})\n`; symDef += ` (name "${p.name}" (effects (font (size 1.27 1.27))))\n`; symDef += ` (number "${p.num}" (effects (font (size 1.27 1.27))))\n`; symDef += ` )\n`; }); symDef += ` )\n`; } else { const compInst = createComp(c); const geo = compInst.geometry(); const hw = (geo.w / 2) * K_SCALE; const hh = (geo.h / 2) * K_SCALE; symDef += ` (symbol "${libData.symName}_0_1"\n`; symDef += ` (rectangle (start -${hw.toFixed(3)} -${hh.toFixed(3)}) (end ${hw.toFixed(3)} ${hh.toFixed(3)})\n`; symDef += ` (stroke (width 0.254) (type default)) (fill (type background))\n )\n )\n`; symDef += ` (symbol "${libData.symName}_1_1"\n`; compInst._pinLocalCoords().forEach((pinCoord, i) => { const px = pinCoord.ldx * K_SCALE; const py = -pinCoord.ldy * K_SCALE; const stubLen = compInst.stubLen(i) * K_SCALE; const connX = px + (pinCoord.ex * stubLen); const connY = py + (-pinCoord.ey * stubLen); let kAngle = 0; if (pinCoord.ex > 0.5) kAngle = 180; else if (pinCoord.ex < -0.5) kAngle = 0; else if (pinCoord.ey < -0.5) kAngle = 270; else if (pinCoord.ey > 0.5) kAngle = 90; const num = pinCoord.gp.name || String(i + 1); symDef += ` (pin passive line (at ${connX.toFixed(3)} ${connY.toFixed(3)} ${kAngle}) (length ${stubLen.toFixed(3)})\n`; symDef += ` (name "${num}" (effects (font (size 1.27 1.27))))\n`; symDef += ` (number "${num}" (effects (font (size 1.27 1.27))))\n )\n`; }); symDef += ` )\n`; } symDef += ` )\n`; usedLibs.set(libData.libId, symDef); } usedLibs.forEach(def => { o += def; }); o += ` )\n`; // Component Instantiation on the Paper S.components.forEach(c => { const libData = compLibMap.get(c.id); const cx = (c.x - centerOffX) * K_SCALE + K_OFFSET_X; const cy = (c.y - centerOffY) * K_SCALE + K_OFFSET_Y; let rot = (-c.rotation || 0) % 360; if (rot < 0) rot += 360; let mirrorStr = c.flipX ? " (mirror y)" : ""; let textAngle = (rot === 90 || rot === 270) ? 90 : 0; const hasValue = !!c.value; const val = c.value || libData.symName; const valHide = hasValue ? "" : " hide"; const compUuid = uuid(); let refX = cx + 2, refY = cy - 2; let valX = cx + 2, valY = cy + 2; if (S.componentLabels && S.componentLabels[c.id]) { const hint = S.componentLabels[c.id]; refX = (hint.refPosition.x - centerOffX) * K_SCALE + K_OFFSET_X; refY = (hint.refPosition.y - centerOffY) * K_SCALE + K_OFFSET_Y; valX = (hint.valuePosition.x - centerOffX) * K_SCALE + K_OFFSET_X; valY = (hint.valuePosition.y - centerOffY) * K_SCALE + K_OFFSET_Y; } o += ` (symbol (lib_id "${libData.libId}") (at ${cx.toFixed(3)} ${cy.toFixed(3)} ${rot})${mirrorStr} (unit 1)\n`; o += ` (in_bom yes) (on_board yes) (dnp no) (fields_autoplaced no) (uuid "${compUuid}")\n`; // Write all dynamic instance properties let propId = 0; const instProps = { "Reference": c.ref, "Value": val, "Footprint": libData.footprint || "", "Datasheet": libData.datasheet || "~", ...(libData.props || {}) };['Reference', 'Value', 'Footprint', 'Datasheet'].forEach(k => { let pX = cx, pY = cy; if (k === 'Reference') { pX = refX; pY = refY; } else if (k === 'Value') { pX = valX; pY = valY; } let hideStr = (k === 'Footprint' || k === 'Datasheet' || (k === 'Value' && valHide)) ? ' hide' : ''; o += ` (property "${k}" "${String(instProps[k] || '').replace(/"/g, '\\"')}" (id ${propId++}) (at ${pX.toFixed(3)} ${pY.toFixed(3)} ${textAngle})\n`; o += ` (effects (font (size 1.27 1.27))${hideStr})\n )\n`; delete instProps[k]; }); for (const [k, v] of Object.entries(instProps)) { if (!k.startsWith('ki_')) { o += ` (property "${k}" "${String(v).replace(/"/g, '\\"')}" (id ${propId++}) (at ${cx.toFixed(3)} ${cy.toFixed(3)} ${textAngle})\n`; o += ` (effects (font (size 1.27 1.27)) hide)\n )\n`; } } o += ` (instances\n`; o += ` (project ""\n`; o += ` (path "/${sheetUuid}"\n`; o += ` (reference "${c.ref}") (unit 1)\n`; o += ` )\n`; o += ` )\n`; o += ` )\n`; o += ` )\n`; }); S.wires.forEach(w => { if (!w.points || w.points.length < 2) return; for (let i = 0; i < w.points.length - 1; i++) { const p1 = w.points[i]; const p2 = w.points[i+1]; const x1 = (p1.x - centerOffX) * K_SCALE + K_OFFSET_X; const y1 = (p1.y - centerOffY) * K_SCALE + K_OFFSET_Y; const x2 = (p2.x - centerOffX) * K_SCALE + K_OFFSET_X; const y2 = (p2.y - centerOffY) * K_SCALE + K_OFFSET_Y; o += ` (wire (pts (xy ${x1.toFixed(3)} ${y1.toFixed(3)}) (xy ${x2.toFixed(3)} ${y2.toFixed(3)}))\n`; o += ` (stroke (width 0) (type default)) (uuid "${uuid()}")\n )\n`; } }); if (S.junctionPoints) { S.junctionPoints.forEach(j => { const jx = (j.x - centerOffX) * K_SCALE + K_OFFSET_X; const jy = (j.y - centerOffY) * K_SCALE + K_OFFSET_Y; o += ` (junction (at ${jx.toFixed(3)} ${jy.toFixed(3)}) (diameter 0) (color 0 0 0 0) (uuid "${uuid()}"))\n`; }); } o += `)\n`; return o; } // ── MicroCAP Schematic (.cir) ───────────────────────────────── export function exportSpice() { const M_SCALE = 8 / 20; // Scale our 20px grid to an 8-unit MicroCap schematic grid const M_OFFSET = 200; // Center offset for MicroCap let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; S.components.forEach(c => { minX = Math.min(minX, c.x); maxX = Math.max(maxX, c.x); minY = Math.min(minY, c.y); maxY = Math.max(maxY, c.y); }); if (minX === Infinity) { minX = 0; maxX = 0; minY = 0; maxY = 0; } const centerOffX = (minX + maxX) / 2; const centerOffY = (minY + maxY) / 2; let o = `[Main]\nFileType=CIR\nVersion=12.00\nProgram=Micro-Cap\n\n`; const getMcName = (type) => { switch(type) { case 'R': return 'Resistor'; case 'C': return 'Capacitor'; case 'L': return 'Inductor'; case 'D': return 'Diode'; case 'Z': return 'Zener'; default: return 'Macro'; } }; S.components.forEach(c => { const mcName = getMcName(c.type); const cx = Math.round((c.x - centerOffX) * M_SCALE) + M_OFFSET; const cy = Math.round((c.y - centerOffY) * M_SCALE) + M_OFFSET; let rot = Math.round((c.rotation || 0) / 90) % 4; if (rot < 0) rot += 4; if (c.flipX) rot += 4; o += `[Comp]\nName=${mcName}\nPx=${cx},${cy}\nRot=${rot}\n`; o += `[Attr]\nON=10,-10,PART\nV=${c.ref}\n`; o += `[Attr]\nON=10,10,VALUE\nV=${c.value || '1k'}\n\n`; }); S.wires.forEach(w => { if (!w.points || w.points.length < 2) return; for (let i = 0; i < w.points.length - 1; i++) { const p1 = w.points[i]; const p2 = w.points[i+1]; const x1 = Math.round((p1.x - centerOffX) * M_SCALE) + M_OFFSET; const y1 = Math.round((p1.y - centerOffY) * M_SCALE) + M_OFFSET; const x2 = Math.round((p2.x - centerOffX) * M_SCALE) + M_OFFSET; const y2 = Math.round((p2.y - centerOffY) * M_SCALE) + M_OFFSET; o += `[Wire]\nPxs=${x1},${y1},${x2},${y2}\n\n`; } }); return o; } // ── postMessage listener ────────────────────────────────────── let _loadBoard = null; export function setIOCallbacks(loadBoard) { _loadBoard = loadBoard; } export function initPostMessageListener() { window.addEventListener('message', e => { // console.log('[schema/io] message received:', e.data?.type, e.origin); const d = e.data; if (!d || typeof d !== 'object') return; if (d.type === 'SCHEMA_INIT') { window._embeddedMode = true; // Safely cache data for title block exports window._boardId = d.boardId || null; window._deviceId = d.deviceId || null; const elementsToHide =['btn-load-db', 'btn-import-file', 'es-load-btn']; elementsToHide.forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; }); window.parent.postMessage({ type: 'SCHEMA_READY' }, '*'); if (_loadBoard) _loadBoard(d.boardId || null, d.deviceId || null); } else if (d.type === 'SCHEMA_RESIZE') { import('./app.js').then(({ triggerResize }) => triggerResize()); } }); }