UNPKG

@typecad/schematic

Version:

Generate KiCAD schematics from your typeCAD project

387 lines (335 loc) 19.3 kB
import fs from "node:fs"; import fsexp from "fast-sexpr"; import SExpr from "s-expression.js"; import { platform } from 'node:os'; import { kicad_path } from "./kicad"; import { GRID_UNIT_MM } from "./kicad-schematic"; const S = new SExpr(); export interface SymbolDefinition { rawSexpr: any[]; serializedLibEntry: string; dimensionsMM?: { widthMM: number, heightMM: number, minX: number, minY: number }; // Dimensions and top-left offset relative to symbol origin (all in mm) } export interface PinLocation { x: number; y: number; angle: number; } export class SymbolLibraryManager { private libraryCache: Map<string, any> = new Map(); // Cache for parsed library files (key: lib_name) private symbolCache: Map<string, SymbolDefinition> = new Map(); // Cache for specific symbol definitions (key: symbol_fqn) private pinLocationCache: Map<string, PinLocation> = new Map(); // Cache for pin locations (key: symbol_fqn + pin_number) private kicadSymbolPath: string; constructor() { this.kicadSymbolPath = platform() === 'win32' ? `${kicad_path}share/kicad/symbols` : `${kicad_path}symbols`; // Determine path once } private formatSExpression(sexpr: string): string { let formatted = ''; let indentLevel = 0; let inString = false; let i = 0; while (i < sexpr.length) { const char = sexpr[i]; if (char === '"' && (i === 0 || sexpr[i-1] !== '\\')) { inString = !inString; formatted += char; } else if (!inString) { if (char === '(') { if (formatted.length > 0 && formatted[formatted.length - 1] !== '\n' && formatted[formatted.length - 1] !== '\t') { formatted += '\n' + '\t'.repeat(indentLevel); } formatted += char; indentLevel++; } else if (char === ')') { indentLevel--; formatted += char; // Add newline after closing parenthesis if not at end and next char is opening paren if (i + 1 < sexpr.length && sexpr[i + 1] === '(') { formatted += '\n' + '\t'.repeat(indentLevel); } } else if (char === ' ' && sexpr[i + 1] === '(') { // Space before opening paren - add newline and indent formatted += '\n' + '\t'.repeat(indentLevel); } else { formatted += char; } } else { formatted += char; } i++; } return formatted; } private getLibraryContent(libraryName: string): any | null { if (this.libraryCache.has(libraryName)) { return this.libraryCache.get(libraryName); } let symbolFilePath = `${this.kicadSymbolPath}/${libraryName}.kicad_sym`; let symbolFileContents = ""; try { if (fs.existsSync(symbolFilePath)) { symbolFileContents = fs.readFileSync(symbolFilePath, "utf8"); } else { // Fallback to local build lib symbolFilePath = `./build/lib/${libraryName}.kicad_sym`; if (fs.existsSync(symbolFilePath)) { symbolFileContents = fs.readFileSync(symbolFilePath, "utf8"); } else { console.error(`Library file ${libraryName}.kicad_sym not found`); return null; } } // Use fast-sexpr for parsing const parsedLibrary = fsexp(symbolFileContents.replaceAll('"', "`")).pop(); this.libraryCache.set(libraryName, parsedLibrary); return parsedLibrary; } catch (err) { console.error(`Error reading library ${libraryName}`); return null; } } private _parseSymbolDimensions(symbolFqn: string, rawSexpr: any[]): { widthMM: number, heightMM: number, minX: number, minY: number } | null { let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; let foundGraphicalElements = false; function processElement(element: any) { if (!Array.isArray(element)) return; const type = element[0]; if (type === 'rectangle' && element.length > 2) { let startX, startY, endX, endY; for (const prop of element) { if (Array.isArray(prop) && prop.length > 2) { if (prop[0] === 'start' && prop.length === 3) { startX = parseFloat(String(prop[1]).replaceAll('`','')); startY = parseFloat(String(prop[2]).replaceAll('`','')); } else if (prop[0] === 'end' && prop.length === 3) { endX = parseFloat(String(prop[1]).replaceAll('`','')); endY = parseFloat(String(prop[2]).replaceAll('`','')); } } } if (startX !== undefined && startY !== undefined && endX !== undefined && endY !== undefined) { minX = Math.min(minX, startX, endX); minY = Math.min(minY, startY, endY); maxX = Math.max(maxX, startX, endX); maxY = Math.max(maxY, startY, endY); foundGraphicalElements = true; } } else if (type === 'circle' && element.length > 2) { let centerX, centerY, radius; for (const prop of element) { if (Array.isArray(prop) && prop.length > 1) { if (prop[0] === 'center' && prop.length === 3) { centerX = parseFloat(String(prop[1]).replaceAll('`','')); centerY = parseFloat(String(prop[2]).replaceAll('`','')); } else if (prop[0] === 'radius' && prop.length === 2) { radius = parseFloat(String(prop[1]).replaceAll('`','')); } } } if (centerX !== undefined && centerY !== undefined && radius !== undefined) { minX = Math.min(minX, centerX - radius); minY = Math.min(minY, centerY - radius); maxX = Math.max(maxX, centerX + radius); maxY = Math.max(maxY, centerY + radius); foundGraphicalElements = true; } } else if (type === 'pin') { // Consider pin locations for bounding box let atData: number[] | null = null; for(const prop of element) { if (!Array.isArray(prop)) continue; if (prop[0] === 'at' && prop.length > 2) { atData = [parseFloat(String(prop[1]).replaceAll('`','')), parseFloat(String(prop[2]).replaceAll('`',''))]; } } if (atData) { minX = Math.min(minX, atData[0]); minY = Math.min(minY, atData[1]); maxX = Math.max(maxX, atData[0]); maxY = Math.max(maxY, atData[1]); foundGraphicalElements = true; } } else if (type === 'symbol' && Array.isArray(element[2])) { for (let i = 2; i < element.length; i++) { processElement(element[i]); } } } for (const item of rawSexpr) { processElement(item); } if (!foundGraphicalElements || minX === Infinity) { if (!foundGraphicalElements) { console.warn(`No graphical elements or pins found for dimension parsing in symbol ${symbolFqn}. Using minimal default size.`); } // Default to a small size (e.g., 1x1 grid unit) if no info, centered at origin return { widthMM: GRID_UNIT_MM, heightMM: GRID_UNIT_MM, minX: -GRID_UNIT_MM / 2, minY: -GRID_UNIT_MM / 2 }; } // Handle cases where pins might be at 0,0 only if (minX === Infinity) { minX = 0; maxX = 0; } if (minY === Infinity) { minY = 0; maxY = 0; } if (maxX === -Infinity) maxX = minX; // if only minX was set from a single point if (maxY === -Infinity) maxY = minY; return { widthMM: Math.max(GRID_UNIT_MM * 0.25, maxX - minX), // Ensure a minimum sensible size heightMM: Math.max(GRID_UNIT_MM * 0.25, maxY - minY), minX: minX, minY: minY }; } private findSymbolInLibrary(libraryContent: any, libraryName: string, symbolName: string): SymbolDefinition | null { const symbolFqn = `${libraryName}:${symbolName}`; if (this.symbolCache.has(symbolFqn)) { return this.symbolCache.get(symbolFqn)!; } for (const item of libraryContent) { if (Array.isArray(item) && item.length > 1 && item[0] === 'symbol') { // Check for the specific symbol name (removing potential backticks from fsexp output) const currentSymbolName = String(item[1]).replaceAll('`',''); if (currentSymbolName === symbolName) { const rawSexpr = JSON.parse(JSON.stringify(item)); // Deep clone the array to avoid reference issues // Modify the name to be fully qualified *in the cloned array* rawSexpr[1] = `"${symbolFqn}"`; // Handle 'extends' const extendsIndex = rawSexpr.findIndex(el => Array.isArray(el) && el[0] === 'extends'); if (extendsIndex !== -1 && Array.isArray(rawSexpr[extendsIndex]) && rawSexpr[extendsIndex].length > 1) { const baseSymbolName = String(rawSexpr[extendsIndex][1]).replaceAll('`', ''); // Modify the extends part in the cloned array rawSexpr[extendsIndex][1] = `"${libraryName}:${baseSymbolName}"`; // Recursively find the base symbol definition // console.log(`Symbol ${symbolFqn} extends ${libraryName}:${baseSymbolName}. Following extension...`); // Use getSymbolDefinition which includes caching const baseSymbolDef = this.getSymbolDefinition(`${libraryName}:${baseSymbolName}`); if (baseSymbolDef) { // Create a new definition for the extended symbol with the correct name // but using the base symbol's content let updatedSerializedEntry = baseSymbolDef.serializedLibEntry; // Replace all occurrences of the base symbol name with the extended symbol name // This includes the main symbol name and nested symbol unit names const baseSymbolPattern = new RegExp(`"${libraryName}:${baseSymbolName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`, 'g'); updatedSerializedEntry = updatedSerializedEntry.replace(baseSymbolPattern, `"${symbolFqn}"`); // Also replace symbol unit names (e.g., ATtiny807-M_0_1 -> ATtiny3227-M_0_1) const baseUnitPattern = new RegExp(`"${baseSymbolName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}_`, 'g'); updatedSerializedEntry = updatedSerializedEntry.replace(baseUnitPattern, `"${symbolName}_`); const extendedSymbolDef: SymbolDefinition = { rawSexpr: baseSymbolDef.rawSexpr, serializedLibEntry: updatedSerializedEntry, dimensionsMM: baseSymbolDef.dimensionsMM }; // Cache and return the extended symbol definition this.symbolCache.set(symbolFqn, extendedSymbolDef); return extendedSymbolDef; } else { console.error(`ERROR: Base symbol ${libraryName}:${baseSymbolName} for ${symbolFqn} not found.`); return null; } } // Serialize the modified s-expression for the library entry let serializedLibEntry = S.serialize(rawSexpr).replaceAll("`", '"'); // Ensure quotes are correct // Format the serialized entry with proper indentation for KiCAD serializedLibEntry = this.formatSExpression(serializedLibEntry); const dimensions = this._parseSymbolDimensions(symbolFqn, rawSexpr); // Parse dimensions const definition: SymbolDefinition = { rawSexpr: rawSexpr, serializedLibEntry: serializedLibEntry, dimensionsMM: dimensions ?? undefined // Store dimensions }; this.symbolCache.set(symbolFqn, definition); return definition; } } } return null; } public getSymbolDefinition(symbolFqn: string): SymbolDefinition | null { if (this.symbolCache.has(symbolFqn)) { return this.symbolCache.get(symbolFqn)!; } const parts = symbolFqn.split(':'); if (parts.length !== 2) { console.error(`Invalid fully qualified symbol name: ${symbolFqn}`); return null; } const [libraryName, symbolName] = parts; const libraryContent = this.getLibraryContent(libraryName); if (!libraryContent) { return null; // Error already logged } const definition = this.findSymbolInLibrary(libraryContent, libraryName, symbolName); if (!definition) { console.error(`ERROR: Symbol definition ${symbolFqn} not found in library ${libraryName}.`); } return definition; } public getPinLocation(symbolFqn: string, pinNumber: string | number): PinLocation | null { const cacheKey = `${symbolFqn}_${pinNumber}`; if (this.pinLocationCache.has(cacheKey)) { return this.pinLocationCache.get(cacheKey)!; } const symbolDef = this.getSymbolDefinition(symbolFqn); if (!symbolDef) { // Error logged in getSymbolDefinition // console.error(`Cannot get pin location for non-existent symbol: ${symbolFqn}`); return null; } // Search within the raw S-expression for the pin data // This needs to accommodate the different structures observed in the original code try { // Iterate through the elements of the symbol definition for (const element of symbolDef.rawSexpr) { if (!Array.isArray(element)) continue; // Structure 1: (pin type name (at x y a) (length l) (name "n" (effects ..)) (number "num" (effects ..))) if (element[0] === 'pin') { let currentPinNumber: string | null = null; let atData: number[] | null = null; for(const prop of element) { if (!Array.isArray(prop)) continue; if (prop[0] === 'number' && prop.length > 1) { currentPinNumber = String(prop[1]).replaceAll('`',''); } else if (prop[0] === 'at' && prop.length > 3) { atData = [parseFloat(prop[1]), parseFloat(prop[2]), parseFloat(prop[3])]; } } if (currentPinNumber === String(pinNumber) && atData) { const location: PinLocation = { x: atData[0], y: atData[1], angle: atData[2] }; this.pinLocationCache.set(cacheKey, location); return location; } } // Structure 2: Check within nested elements like 'symbol BLAH_1_1 ... (pin ...)' // This handles symbols composed of multiple units/bodies where pins are defined within a nested symbol element // e.g., ["symbol", "`NAME_1_1`", ["pin", ...], ["pin", ...]] else if (Array.isArray(element) && element.length > 1 && element[0] === 'symbol' && typeof element[1] === 'string') { // Iterate through the children of this nested symbol element for (const subElement of element.slice(2)) { // Skip the "symbol" and name parts if (!Array.isArray(subElement) || subElement.length === 0 || subElement[0] !== 'pin') continue; let currentPinNumber: string | null = null; let atData: number[] | null = null; for(const prop of subElement) { if (!Array.isArray(prop)) continue; if (prop[0] === 'number' && prop.length > 1) { currentPinNumber = String(prop[1]).replaceAll('`',''); } else if (prop[0] === 'at' && prop.length > 3) { atData = [parseFloat(prop[1]), parseFloat(prop[2]), parseFloat(prop[3])]; } } if (currentPinNumber === String(pinNumber) && atData) { const location: PinLocation = { x: atData[0], y: atData[1], angle: atData[2] }; this.pinLocationCache.set(cacheKey, location); return location; } } } } } catch (e) { console.error(`Error parsing pin data for ${symbolFqn} pin ${pinNumber}:`, e); return null; } console.error(`ERROR: Pin ${pinNumber} not found in symbol ${symbolFqn}`); // Log the structure we attempted to parse console.error(`Failed structure for ${symbolFqn}:`, JSON.stringify(symbolDef?.rawSexpr, null, 2)); return null; } }