UNPKG

@typecad/kicad2typecad

Version:

Generate typeCAD code from KiCAD PCB files

523 lines (472 loc) 21.6 kB
#!/usr/bin/env tsx 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"); }