UNPKG

@typecad/kicad2typecad

Version:

Generate typeCAD code from KiCAD PCB files

412 lines (378 loc) 16.8 kB
#!/usr/bin/env tsx 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>"); }