@typecad/kicad2typecad
Version:
Generate typeCAD code from KiCAD PCB files
523 lines (472 loc) • 21.6 kB
text/typescript
import { PCB } from '@typecad/typecad';
import { TrackBuilder } from '@typecad/typecad';
import fsexp from 'fast-sexpr';
import * as fs from 'node:fs';
import chalk from 'chalk';
interface KicadSegment {
start: { x: number; y: number };
end: { x: number; y: number };
width: number;
layer: string;
net?: number;
uuid?: string;
}
function parseSexprItem(item: any[]): KicadSegment | null {
if (!Array.isArray(item) || item[0] !== 'segment') {
return null;
}
let start: { x: number; y: number } | undefined;
let end: { x: number; y: number } | undefined;
let width: number = 0.2;
let layer: string = "F.Cu";
let net: number | undefined;
let uuid: string | undefined;
for (let i = 1; i < item.length; i++) {
const element = item[i];
if (Array.isArray(element) && element.length > 0) {
const key = element[0];
const value = element.slice(1);
switch (key) {
case 'start':
if (value.length === 2) {
const x = parseFloat(value[0]);
const y = parseFloat(value[1]);
if (!isNaN(x) && !isNaN(y)) {
start = { x, y };
}
}
break;
case 'end':
if (value.length === 2) {
const x = parseFloat(value[0]);
const y = parseFloat(value[1]);
if (!isNaN(x) && !isNaN(y)) {
end = { x, y };
}
}
break;
case 'width':
if (value.length === 1) {
const w = parseFloat(value[0]);
if (!isNaN(w)) {
width = w;
}
}
break;
case 'layer':
if (value.length === 1 && typeof value[0] === 'string') {
layer = value[0].replace(/"/g, '');
}
break;
case 'net':
if (value.length === 1) {
const n = parseInt(value[0], 10);
if (!isNaN(n)) {
net = n;
}
}
break;
case 'uuid':
if (value.length === 1 && typeof value[0] === 'string') {
uuid = value[0].replace(/"/g, '');
}
break;
}
}
}
if (start && end) {
const result: KicadSegment = { start, end, width, layer, net, uuid };
return result;
}
return null;
}
function findSegments(parsedData: any, depth = 0): KicadSegment[] {
const segments: KicadSegment[] = [];
if (Array.isArray(parsedData)) {
for (let i = 0; i < parsedData.length; i++) {
const item = parsedData[i];
if (Array.isArray(item) && item.length > 0 && item[0] === 'segment') {
const segment = parseSexprItem(item);
if (segment) {
segments.push(segment);
}
} else if (Array.isArray(item)) {
segments.push(...findSegments(item, depth + 1));
}
}
}
return segments;
}
interface KicadComponentPlacement {
reference: string;
x: number;
y: number;
rotation: number;
layer: string;
}
function parseFootprintItem(item: any[]): KicadComponentPlacement | null {
if (!Array.isArray(item) || item[0] !== 'footprint') {
return null;
}
let reference: string | undefined;
let x: number | undefined;
let y: number | undefined;
let rotation: number = 0;
let layer: string | undefined;
for (let i = 1; i < item.length; i++) {
const element = item[i];
if (Array.isArray(element) && element.length > 0) {
const key = element[0].toString().replace(/"/g, '');
const values = element.slice(1);
switch (key) {
case 'layer':
if (typeof values[0] === 'string') {
layer = values[0].replace(/"/g, '');
}
break;
case 'at':
if (values.length >= 2) {
x = parseFloat(values[0]);
y = parseFloat(values[1]);
if (values.length > 2) {
rotation = parseFloat(values[2]);
}
if (isNaN(x) || isNaN(y) || isNaN(rotation)) {
x = undefined; y = undefined; rotation = 0;
}
}
break;
case 'property':
if (values.length > 1 && typeof values[0] === 'string' && values[0].replace(/"/g, '') === 'Reference') {
if (typeof values[1] === 'string') {
reference = values[1].replace(/"/g, '');
}
}
break;
}
}
}
if (reference && x !== undefined && y !== undefined && layer) {
return { reference, x, y, rotation, layer };
}
return null;
}
function findFootprints(parsedData: any, depth = 0): KicadComponentPlacement[] {
const placements: KicadComponentPlacement[] = [];
if (Array.isArray(parsedData)) {
for (let i = 0; i < parsedData.length; i++) {
const item = parsedData[i];
if (Array.isArray(item) && item.length > 0 && item[0] === 'footprint') {
const placement = parseFootprintItem(item);
if (placement) {
placements.push(placement);
}
} else if (Array.isArray(item)) {
placements.push(...findFootprints(item, depth + 1));
}
}
}
return placements;
}
interface KicadVia {
at: { x: number; y: number };
size: number;
drill: number;
layers?: string[];
net?: number;
uuid?: string;
}
function parseViaItem(item: any[]): KicadVia | null {
if (!Array.isArray(item) || item[0] !== 'via') {
return null;
}
let at: { x: number; y: number } | undefined;
let size: number | undefined;
let drill: number | undefined;
let layers: string[] | undefined;
let net: number | undefined;
let uuid: string | undefined;
for (let i = 1; i < item.length; i++) {
const element = item[i];
if (Array.isArray(element) && element.length > 0) {
const key = element[0].toString().replace(/"/g, '');
const values = element.slice(1);
switch (key) {
case 'at':
if (values.length === 2) {
const x = parseFloat(values[0]);
const y = parseFloat(values[1]);
if (!isNaN(x) && !isNaN(y)) at = { x, y };
}
break;
case 'size':
if (values.length === 1) {
const s = parseFloat(values[0]);
if (!isNaN(s)) size = s;
}
break;
case 'drill':
if (values.length === 1) {
const d = parseFloat(values[0]);
if (!isNaN(d)) drill = d;
}
break;
case 'layers':
if (Array.isArray(values) && values.every(v => typeof v === 'string')) {
layers = values.map(v => v.replace(/"/g, ''));
}
break;
case 'net':
if (values.length === 1) {
const n = parseInt(values[0], 10);
if (!isNaN(n)) net = n;
}
break;
case 'uuid':
if (values.length === 1 && typeof values[0] === 'string') {
uuid = values[0].replace(/"/g, '');
}
break;
}
}
}
if (at && size !== undefined && drill !== undefined) {
return { at, size, drill, layers, net, uuid };
}
return null;
}
function findVias(parsedData: any, depth = 0): KicadVia[] {
const vias: KicadVia[] = [];
if (Array.isArray(parsedData)) {
for (let i = 0; i < parsedData.length; i++) {
const item = parsedData[i];
if (Array.isArray(item) && item.length > 0 && item[0] === 'via') {
const via = parseViaItem(item);
if (via) {
vias.push(via);
}
} else if (Array.isArray(item)) {
vias.push(...findVias(item, depth + 1));
}
}
}
return vias;
}
// Parse TypeScript files to extract variable names and their component types
function extractVariableNamesFromTypeScript(tsFilePath: string): Record<string, string> {
if (!fs.existsSync(tsFilePath)) {
return {};
}
try {
const tsContent = fs.readFileSync(tsFilePath, 'utf-8');
// First, collect all component declarations by type
const componentsByType: Record<string, string[]> = {};
// Simple regex patterns to match common component declarations
const patterns = [
// let variableName = new ComponentType(...)
/let\s+(\w+)\s*=\s*new\s+(\w+)\s*\(/g,
// const variableName = new ComponentType(...)
/const\s+(\w+)\s*=\s*new\s+(\w+)\s*\(/g,
// this.variableName = new ComponentType(...)
/this\.(\w+)\s*=\s*new\s+(\w+)\s*\(/g
];
patterns.forEach(pattern => {
let match;
while ((match = pattern.exec(tsContent)) !== null) {
const variableName = match[1];
const componentType = match[2];
if (!componentsByType[componentType]) {
componentsByType[componentType] = [];
}
componentsByType[componentType].push(variableName);
}
});
return componentsByType;
} catch (error) {
console.warn(`Warning: Could not parse TypeScript file ${tsFilePath}`);
return {};
}
}
function createReferenceMapping(componentsByType: Record<string, string[]>, kicadReferences: string[]): Record<string, string> {
const mapping: Record<string, string> = {};
// Heuristic mapping from component types to reference prefixes
const typeToPrefix: Record<string, string> = {
'Resistor': 'R',
'Capacitor': 'C',
'Inductor': 'L',
'Diode': 'D',
'LED': 'D',
'Transistor': 'Q',
'MOSFET': 'Q',
'IC': 'U',
'Microcontroller': 'U'
};
// For each component type found in TypeScript
Object.entries(componentsByType).forEach(([componentType, variableNames]) => {
const prefix = typeToPrefix[componentType];
if (!prefix) return;
// Find all KiCad references that match this prefix
const matchingRefs = kicadReferences
.filter(ref => ref.startsWith(prefix))
.sort(); // Sort to ensure consistent ordering (R1, R2, R3, etc.)
// Map them in order
variableNames.forEach((variableName, index) => {
if (index < matchingRefs.length) {
mapping[matchingRefs[index]] = variableName;
}
});
});
return mapping;
}
async function kicadDataToTypeCAD(filePath: string, tsFilePath?: string) {
let sExpressionText: string;
const sourceDescription: string = `File: ${filePath}`;
// We'll create the reference mapping after we parse the KiCad file and know what references exist
let componentsByType: Record<string, string[]> = {};
if (tsFilePath) {
componentsByType = extractVariableNamesFromTypeScript(tsFilePath);
if (Object.keys(componentsByType).length > 0) {
console.log(`Found component declarations in ${tsFilePath}:`, componentsByType);
}
} else {
console.log(chalk.yellow(`\n⚠️ Warning: No TypeScript file provided for variable name mapping.`));
console.log(chalk.yellow(` Generated code will use KiCad reference designators (R1, C1, U1, etc.) which may not match your actual variable names.`));
console.log(chalk.yellow(` For accurate variable names, run: npx tsx ./index.ts ${filePath} <your_circuit_file.ts>\n`));
}
try {
console.log(`Reading from ${sourceDescription}`);
if (!fs.existsSync(filePath)) {
console.error(`Error: File not found at ${filePath}`);
return;
}
sExpressionText = fs.readFileSync(filePath, 'utf-8');
if (!sExpressionText.trim()) {
console.log(`${sourceDescription} is empty.`);
return;
}
let rootElement;
try {
const wrappedText = `(${sExpressionText})`;
const parsedArray = fsexp(wrappedText);
if (Array.isArray(parsedArray) && parsedArray.length > 0) {
rootElement = parsedArray.pop();
} else {
throw new Error("Parser did not return a valid array structure.");
}
} catch (error: any) {
console.error(`Failed to parse S-expression from ${sourceDescription}: ${error.message}`);
return;
}
if (!rootElement) {
console.error(`Parsed data from ${sourceDescription} is empty or invalid (rootElement is null/undefined).`);
return;
}
const pcb = new PCB("KicadImportFromFile");
const chKey = chalk.cyan;
const chProp = chalk.magenta;
const chStr = chalk.green;
const chNum = chalk.yellow;
const chPunc = chalk.gray;
const chVar = chalk.blueBright;
const segments = findSegments(rootElement);
if (segments.length > 0) {
console.log(`Found ${segments.length} segments. Generating TrackBuilder chains from ${sourceDescription}.`);
let currentTrackBuilder: TrackBuilder | null = null;
let lastEndPoint: { x: number; y: number } | null = null;
let currentChainLog: string = "";
const allTrackChainLogs: string[] = [];
segments.forEach((segment) => {
if (!currentTrackBuilder || !lastEndPoint || lastEndPoint.x !== segment.start.x || lastEndPoint.y !== segment.start.y) {
if (currentTrackBuilder && currentChainLog) {
allTrackChainLogs.push(currentChainLog + chPunc(')') + chPunc(";"));
}
currentTrackBuilder = pcb.track().from(segment.start, segment.layer, segment.width);
currentChainLog = `${chVar('this.components')}${chPunc('.')}${chKey('push')}${chPunc('(')}${chVar('this.pcb')}${chPunc('.')}${chKey('track')}${chPunc('()')}${chKey('.from')}${chPunc('(')}{ ${chProp('x')}${chPunc(':')} ${chNum(segment.start.x)}${chPunc(',')} ${chProp('y')}${chPunc(':')} ${chNum(segment.start.y)} }${chPunc(',')} ${chStr('"' + segment.layer + '"')}${chPunc(',')} ${chNum(segment.width)}${chPunc(')')}`;
currentTrackBuilder.to({ x: segment.end.x, y: segment.end.y, layer: segment.layer, width: segment.width });
currentChainLog += `${chKey('.to')}${chPunc('(')}{ ${chProp('x')}${chPunc(':')} ${chNum(segment.end.x)}${chPunc(',')} ${chProp('y')}${chPunc(':')} ${chNum(segment.end.y)}${chPunc(',')} ${chProp('layer')}${chPunc(':')} ${chStr('"' + segment.layer + '"')}${chPunc(',')} ${chProp('width')}${chPunc(':')} ${chNum(segment.width)} }${chPunc(')')}`;
} else {
currentTrackBuilder!.to({ x: segment.end.x, y: segment.end.y, layer: segment.layer, width: segment.width });
currentChainLog += `${chKey('.to')}${chPunc('(')}{ ${chProp('x')}${chPunc(':')} ${chNum(segment.end.x)}${chPunc(',')} ${chProp('y')}${chPunc(':')} ${chNum(segment.end.y)}${chPunc(',')} ${chProp('layer')}${chPunc(':')} ${chStr('"' + segment.layer + '"')}${chPunc(',')} ${chProp('width')}${chPunc(':')} ${chNum(segment.width)} }${chPunc(')')}`;
}
lastEndPoint = segment.end;
});
if (currentChainLog) {
allTrackChainLogs.push(currentChainLog + chPunc(')') + chPunc(";"));
}
if (allTrackChainLogs.length > 0) {
console.log(chalk.bold(`\n--- Generated typeCAD TrackBuilder Code from ${sourceDescription} ---`));
allTrackChainLogs.forEach(log => console.log(log));
console.log(chalk.bold("---------------------------------------------------------"));
}
} else {
console.log(`No segments found in ${sourceDescription} content.`);
}
const footprints = findFootprints(rootElement);
if (footprints.length > 0) {
console.log(`Found ${footprints.length} footprints. Generating placement code from ${sourceDescription}.`);
// Create reference mapping now that we know what KiCad references exist
const kicadReferences = footprints.map(fp => fp.reference);
const referenceMapping = createReferenceMapping(componentsByType, kicadReferences);
if (Object.keys(referenceMapping).length > 0) {
console.log(`Created ${Object.keys(referenceMapping).length} reference mappings:`, referenceMapping);
}
const allPlacementLogs: string[] = [];
footprints.forEach(fp => {
// Use mapped variable name if available, otherwise use the KiCad reference as-is
const variableName = referenceMapping[fp.reference] || fp.reference;
let placementLog = `${chVar('this.' + variableName)}${chPunc('.')}${chProp('pcb')} ${chPunc('=')} ${chPunc('{')}`;
placementLog += ` ${chProp('x')}${chPunc(':')} ${chNum(fp.x)}${chPunc(',')}`;
placementLog += ` ${chProp('y')}${chPunc(':')} ${chNum(fp.y)}${chPunc(',')}`;
placementLog += ` ${chProp('rotation')}${chPunc(':')} ${chNum(fp.rotation)}`;
placementLog += ` ${chPunc('}')}${chPunc(';')}`;
allPlacementLogs.push(placementLog);
});
if (allPlacementLogs.length > 0) {
console.log(chalk.bold(`\n--- Generated Component Placement Code from ${sourceDescription} ---`));
allPlacementLogs.forEach(log => console.log(log));
console.log(chalk.bold("-------------------------------------------------------------"));
}
} else {
console.log(`No footprints found in ${sourceDescription} content.`);
}
const vias = findVias(rootElement);
if (vias.length > 0) {
console.log(`Found ${vias.length} vias. Generating typeCAD code from ${sourceDescription}.`);
const allViaLogs: string[] = [];
vias.forEach((via, index) => {
let viaLog = `${chVar('this.v' + (index + 1))} ${chPunc('=')} ${chVar('this.pcb')}${chPunc('.')}${chKey('via')}${chPunc('({')}`;
viaLog += ` ${chProp('at')}${chPunc(': {')} ${chProp('x')}${chPunc(':')} ${chNum(via.at.x)}${chPunc(',')} ${chProp('y')}${chPunc(':')} ${chNum(via.at.y)} ${chPunc('}')}${chPunc(',')}`;
viaLog += ` ${chProp('size')}${chPunc(':')} ${chNum(via.size)}${chPunc(',')}`;
viaLog += ` ${chProp('drill')}${chPunc(':')} ${chNum(via.drill)}`;
viaLog += ` ${chPunc('})')}${chPunc(';')}`;
allViaLogs.push(viaLog);
});
if (allViaLogs.length > 0) {
console.log(chalk.bold(`\n--- Generated typeCAD Via Code from ${sourceDescription} ---`));
allViaLogs.forEach(log => console.log(log));
console.log(chalk.bold("-----------------------------------------------------"));
}
} else {
console.log(`No vias found in ${sourceDescription} content.`);
}
} catch (error: any) {
console.error(`Error processing data from ${sourceDescription}: ${error.message}`);
}
}
const args = process.argv.slice(2);
const filePathArg = args[0];
const tsFileArg = args[1]; // Optional TypeScript file to analyze
if (filePathArg && filePathArg.trim() !== "") {
kicadDataToTypeCAD(filePathArg, tsFileArg);
} else {
console.log("No KiCad file path provided. Please provide a file path as a command line argument.");
console.log("Usage: npx tsx ./index.ts <path_to_kicad_pcb_file> [typescript_file.ts]");
console.log("");
console.log("Options:");
console.log("1. Basic usage (uses KiCad references as-is):");
console.log(" npx tsx ./index.ts board.kicad_pcb");
console.log("");
console.log("2. With TypeScript file analysis:");
console.log(" npx tsx ./index.ts board.kicad_pcb circuit.ts");
}