@typecad/kicad2typecad
Version:
Generate typeCAD code from KiCAD PCB files
412 lines (378 loc) • 16.8 kB
text/typescript
import { PCB } from '@typecad/typecad';
import { TrackBuilder } from '@typecad/typecad';
import fsexp from 'fast-sexpr';
import 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;
}
async function kicadDataToTypeCAD(filePath: string) {
let sExpressionText: string;
const sourceDescription: string = `File: ${filePath}`;
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(";"));
}
currentTrackBuilder = pcb.track().from(segment.start, segment.layer, segment.width);
currentChainLog = `${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(";"));
}
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}.`);
const allPlacementLogs: string[] = [];
footprints.forEach(fp => {
let placementLog = `${chVar('this.' + fp.reference)}${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];
if (filePathArg && filePathArg.trim() !== "") {
kicadDataToTypeCAD(filePathArg);
} 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>");
}