pcb-retrace/docs/schema/kicad.js

207 lines
7.1 KiB
JavaScript

/*
* Copyright (c) 2025-2026 Taras Greben
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
* See LICENSE file for details.
*/
import { db, uuid } from './db.js';
const SCALE = 20 / 2.54;
//const ZIP_URL = 'https://gitlab.com/kicad/libraries/kicad-symbols/-/archive/master/kicad-symbols-master.zip'; // CORS issue
const ZIP_URL = './assets/kicad-symbols-master.zip';
// ── S-Expression Parser ─────────────────────────────────────────
function parseSexpr(str) {
const tokens =[]; let i = 0;
while (i < str.length) {
const c = str[i];
if (c === '(' || c === ')') { tokens.push(c); i++; }
else if (/\s/.test(c)) { i++; }
else if (c === '"') {
let s = '"'; i++;
while (i < str.length) {
if (str[i] === '\\') { s += str[i] + str[i+1]; i += 2; continue; }
if (str[i] === '"') { s += '"'; i++; break; }
s += str[i]; i++;
}
tokens.push(s);
} else {
let s = '';
while (i < str.length && !/[\s()]/.test(str[i]) && str[i] !== '"') { s += str[i]; i++; }
tokens.push(s);
}
}
let ti = 0;
function walk() {
if (ti >= tokens.length) return null;
let t = tokens[ti++];
if (t === '(') {
let list = [];
while (ti < tokens.length && tokens[ti] !== ')') list.push(walk());
ti++; return list;
}
if (t.startsWith('"')) return t.slice(1, -1).replace(/\\"/g, '"');
return t;
}
const ast =[];
while (ti < tokens.length) ast.push(walk());
return ast;
}
// ── Extractors ──────────────────────────────────────────────────
function extractSymbolData(node) {
const rawName = node[1] || '';
const name = rawName.includes(':') ? rawName.split(':').pop() : rawName;
const props = {}, pins =[], graphics =[];
let extendsName = null;
for (let i = 2; i < node.length; i++) {
const child = node[i];
if (!Array.isArray(child)) continue;
const tag = child[0];
if (tag === 'extends') {
extendsName = child[1];
} else if (tag === 'property') {
props[child[1]] = child[2] || '';
} else if (tag === 'pin') {
const at = child.find(x => Array.isArray(x) && x[0] === 'at');
const lengthNode = child.find(x => Array.isArray(x) && x[0] === 'length');
const nameNode = child.find(x => Array.isArray(x) && x[0] === 'name');
const numNode = child.find(x => Array.isArray(x) && x[0] === 'number');
if (at && numNode) {
pins.push({
num: numNode[1],
name: nameNode ? nameNode[1] : '',
x: parseFloat(at[1]) * SCALE,
y: -parseFloat(at[2]) * SCALE,
angle: parseFloat(at[3] || 0),
len: lengthNode ? parseFloat(lengthNode[1]) * SCALE : 20,
electrical_type: typeof child[1] === 'string' ? child[1] : 'passive',
graphical_style: typeof child[2] === 'string' ? child[2] : 'line'
});
}
} else if (tag === 'symbol') {
const nested = extractSymbolData(child);
graphics.push(...nested.graphics);
pins.push(...nested.pins);
} else if (['polyline', 'rectangle', 'circle', 'arc'].includes(tag)) {
graphics.push(child);
}
}
return { name, extendsName, props, pins, graphics };
}
function extractTopLevelSymbols(ast, parsedSymbols) {
const findSymbols = (node) => {
if (!Array.isArray(node)) return;
if (node[0] === 'symbol') {
const data = extractSymbolData(node);
parsedSymbols[data.name] = data;
} else {
for (let i = 0; i < node.length; i++) findSymbols(node[i]);
}
};
findSymbols(ast);
}
function resolveInheritance(parsedSymbols) {
const resolve = (sym) => {
if (!sym.extendsName) return;
const parentName = sym.extendsName.includes(':') ? sym.extendsName.split(':').pop() : sym.extendsName;
const parent = parsedSymbols[parentName];
if (parent) {
if (parent.extendsName) resolve(parent);
sym.pins =[...parent.pins, ...sym.pins];
sym.graphics = [...parent.graphics, ...sym.graphics];
sym.props = { ...parent.props, ...sym.props };
}
sym.extendsName = null;
};
Object.values(parsedSymbols).forEach(sym => resolve(sym));
}
async function upsertParsedSymbols(parsedSymbols, libName) {
const existingLib = await db.getKicadSymbols(libName) ||[];
const existingMap = {};
existingLib.forEach(s => existingMap[s.symbol] = s);
let inserted = 0, updated = 0;
const dbRecords = Object.values(parsedSymbols).map(sym => {
const existing = existingMap[sym.name];
if (existing) updated++; else inserted++;
return {
id: existing ? existing.id : uuid(), // Keep original ID if it exists
library: libName,
symbol: sym.name,
reference: sym.props['Reference'] || '',
description: sym.props['Description'] || '',
datasheet: sym.props['Datasheet'] || '',
keywords: sym.props['ki_keywords'] || '',
fp_filters: sym.props['ki_fp_filters'] || '',
footprint: sym.props['Footprint'] || '',
pinCount: sym.pins.length,
parsedData: JSON.stringify({ pins: sym.pins, graphics: sym.graphics, props: sym.props })
};
});
if (dbRecords.length > 0) await db.saveKicadSymbolsBatch(dbRecords);
return { inserted, updated };
}
// ── Public Importers ──────────────────────────────────────────────
export async function importSelectedFromZip(zip, filenames) {
let stats = { inserted: 0, updated: 0 };
const filesByLib = {};
for (const filename of filenames) {
const match = filename.match(/([^\/]+)\.kicad_symdir\//);
const libName = match ? match[1] : 'Imported';
if (!filesByLib[libName]) filesByLib[libName] = [];
filesByLib[libName].push(filename);
}
for (const [libName, files] of Object.entries(filesByLib)) {
const parsedSymbols = {};
for (const filename of files) {
const text = await zip.files[filename].async("text");
extractTopLevelSymbols(parseSexpr(text), parsedSymbols);
}
resolveInheritance(parsedSymbols);
const res = await upsertParsedSymbols(parsedSymbols, libName);
stats.inserted += res.inserted;
stats.updated += res.updated;
}
return stats;
}
export async function importSymbolsFromText(text, libName = 'Imported') {
const parsedSymbols = {};
extractTopLevelSymbols(parseSexpr(text), parsedSymbols);
resolveInheritance(parsedSymbols);
return await upsertParsedSymbols(parsedSymbols, libName);
}
// ── Auto-Importer for initial boot ─────────────────────────────────
export async function autoImportDeviceLib() {
try {
const existing = await db.getKicadSymbols('Device');
if (existing && existing.length > 0) return;
console.log('[kicad] Auto-importing default Device library...');
const res = await fetch(ZIP_URL);
if (!res.ok) throw new Error('Failed to fetch zip');
const buffer = await res.arrayBuffer();
const zip = await JSZip.loadAsync(buffer);
const deviceFiles = Object.keys(zip.files).filter(name => name.includes('/Device.kicad_symdir/') && name.endsWith('.kicad_sym'));
await importSelectedFromZip(zip, deviceFiles);
console.log('[kicad] Default library import complete!');
} catch (err) {
console.error('[kicad] Auto-import failed:', err);
}
}