UNPKG

@typecad/typecad

Version:

🤖programmatically 💥create 🛰️hardware

313 lines (312 loc) 13.2 kB
// NOTE: This file is a legacy compiled artifact kept for compatibility in some environments. // Vitest/Esm runners may prefer TS source files. Prefer importing from `pad_resolver.ts`. import SExpr from "s-expression.js"; import chalk from "chalk"; import logger from '../../src/logging.js'; const S = new SExpr(); /** * Resolves pin objects to their physical pad positions on the PCB. * Handles coordinate transformation from footprint-relative to board-absolute coordinates. */ export class PadResolver { /** * Get the absolute center position of a pin's pad on the PCB. * * @param pin - The pin to resolve * @returns The absolute X,Y coordinates in mm, or null if unable to resolve * * @example * ```ts * const center = PadResolver.getPadCenter(resistor.pin(1)); * if (center) { * console.log(`Pad is at ${center.x}, ${center.y}`); * } * ``` */ static getPadCenter(pin) { if (!pin.owner) { logger.warn(chalk.yellow(`[PadResolver] Pin ${pin.number} has no owner component`)); return null; } const geometry = this.getPadGeometry(pin.owner, pin.number); if (!geometry) { return null; } return { x: geometry.center.x, y: geometry.center.y, layer: geometry.layer }; } /** * Get complete geometry information for a specific pad. * * @param component - The component containing the pad * @param pinNumber - The pin/pad number to look up * @returns Complete pad geometry, or null if not found * * @example * ```ts * const padGeom = PadResolver.getPadGeometry(resistor, 1); * if (padGeom) { * console.log(`Pad shape: ${padGeom.shape}, size: ${padGeom.size.width}x${padGeom.size.height}mm`); * } * ``` */ static getPadGeometry(component, pinNumber) { try { // Get the footprint S-expression const footprintSExpr = component.footprint_lib(component.footprint); // Parse the S-expression const parsed = S.parse(footprintSExpr); if (!Array.isArray(parsed) || parsed.length === 0) { logger.warn(chalk.yellow(`[PadResolver] Unable to parse footprint for ${component.reference}`)); return null; } // Find the pad matching the pin number const padNode = this.findPadNode(parsed, pinNumber); if (!padNode) { logger.warn(chalk.yellow(`[PadResolver] Pad ${pinNumber} not found in footprint ${component.footprint} for ${component.reference}`)); return null; } // Extract pad data from the S-expression const relativeData = this.extractPadData(padNode); if (!relativeData) { logger.warn(chalk.yellow(`[PadResolver] Unable to extract pad data for ${component.reference} pin ${pinNumber}`)); return null; } // Transform from footprint-relative to board-absolute coordinates const absoluteGeometry = this.transformToAbsolute(relativeData, component); return absoluteGeometry; } catch (error) { logger.warn(chalk.yellow(`[PadResolver] Error resolving pad geometry for ${component.reference} pin ${pinNumber}: ${error.message}`)); return null; } } /** * Find a pad node in the footprint S-expression tree. * @private */ static findPadNode(sexpr, pinNumber) { // Normalize pin number to string for comparison const targetNumber = String(pinNumber); // Recursively search for pad nodes const search = (node) => { if (!Array.isArray(node)) return null; // Check if this is a pad node: (pad "number" ...) if (node[0] === 'pad' && node.length > 1) { const padNumber = String(node[1]).replace(/[`"]/g, ''); if (padNumber === targetNumber) { return node; } } // Search children for (const child of node) { if (Array.isArray(child)) { const result = search(child); if (result) return result; } } return null; }; return search(sexpr); } /** * Extract pad data from a pad S-expression node. * @private */ static extractPadData(padNode) { let at = { x: 0, y: 0, rotation: 0 }; let size = { width: 1.0, height: 1.0 }; let shape = 'rect'; let type = 'smd'; let layers = ['F.Cu']; const number = String(padNode[1]).replace(/[`"]/g, ''); // Parse the pad node elements for (const element of padNode) { if (!Array.isArray(element)) continue; const elementType = element[0]; if (elementType === 'at') { // (at x y [rotation]) at.x = parseFloat(String(element[1])) || 0; at.y = parseFloat(String(element[2])) || 0; at.rotation = element.length > 3 ? parseFloat(String(element[3])) || 0 : 0; } else if (elementType === 'size') { // (size width height) size.width = parseFloat(String(element[1])) || 1.0; size.height = parseFloat(String(element[2])) || 1.0; } else if (elementType === 'layers') { // (layers "F.Cu" "F.Paste" "F.Mask") layers = element.slice(1).map((l) => String(l).replace(/[`"]/g, '')); } } // Type is the 2nd element: (pad "1" thru_hole circle ...) if (padNode.length > 2) { type = String(padNode[2]).replace(/[`"]/g, ''); } // Shape is the 3rd element: (pad "1" type shape ...) if (padNode.length > 3) { shape = String(padNode[3]).replace(/[`"]/g, ''); } return { number, type, at, size, shape, layers }; } /** * Transform pad data from footprint-relative to board-absolute coordinates. * @private */ static transformToAbsolute(relativeData, component) { if (!relativeData) { throw new Error('Invalid relative data'); } const componentX = component.pcb.x || 0; const componentY = component.pcb.y || 0; const componentRotation = component.pcb.rotation || 0; const componentSide = component.pcb.side || 'front'; // Convert rotation to radians // KiCad uses clockwise rotation (Y-down coordinate system), so we negate the angle const rotRad = (-componentRotation * Math.PI) / 180; const cosRot = Math.cos(rotRad); const sinRot = Math.sin(rotRad); // Apply rotation transformation to pad position let padX = relativeData.at.x; let padY = relativeData.at.y; // For back side, flip X coordinate (KiCad convention) if (componentSide === 'back') { padX = -padX; } // Rotate the pad position around component origin // Using standard rotation matrix with negated angle for KiCad's clockwise rotation const rotatedX = padX * cosRot - padY * sinRot; const rotatedY = padX * sinRot + padY * cosRot; // Translate to absolute board coordinates const absoluteX = componentX + rotatedX; const absoluteY = componentY + rotatedY; // Calculate absolute rotation let absoluteRotation = componentRotation + relativeData.at.rotation; if (componentSide === 'back') { absoluteRotation = (360 - absoluteRotation) % 360; } absoluteRotation = absoluteRotation % 360; // Determine the primary copper layer and all layers const copperLayers = relativeData.layers.filter(l => l.includes('.Cu')); const hasWildcardCu = copperLayers.some(l => l === '*.Cu'); let primaryLayer = 'F.Cu'; let allLayers = []; // Normalize pad type let normalizedType = 'smd'; const typeStr = relativeData.type.toLowerCase(); if (typeStr.includes('thru_hole')) { normalizedType = 'thru_hole'; } else if (typeStr.includes('np_thru_hole')) { normalizedType = 'np_thru_hole'; } else if (typeStr.includes('connect')) { normalizedType = 'connect'; } // For through-hole pads, the pad exists on all copper layers if (normalizedType === 'thru_hole' || normalizedType === 'np_thru_hole') { // Through-hole pads exist on all copper layers allLayers = ['F.Cu', 'B.Cu', 'In1.Cu', 'In2.Cu', 'In3.Cu', 'In4.Cu']; // Add more inner layers as needed primaryLayer = componentSide === 'back' ? 'B.Cu' : 'F.Cu'; } else { // SMD pads exist only on the specified layers if (hasWildcardCu) { // KiCad may specify '*.Cu' for pads that exist on all copper layers (e.g., 'connect'). // For routing, avoid propagating the wildcard; expand to the two outer copper layers by default. allLayers = ['F.Cu', 'B.Cu']; primaryLayer = componentSide === 'back' ? 'B.Cu' : 'F.Cu'; } else if (copperLayers.length > 0) { primaryLayer = copperLayers[0]; // Map layer to component side if (componentSide === 'back') { if (primaryLayer === 'F.Cu') primaryLayer = 'B.Cu'; else if (primaryLayer === 'B.Cu') primaryLayer = 'F.Cu'; } allLayers = [primaryLayer]; } else { // Fallback if no copper layer specified: assume outer layer based on component side primaryLayer = componentSide === 'back' ? 'B.Cu' : 'F.Cu'; allLayers = [primaryLayer]; } } // Normalize shape name let normalizedShape = 'rect'; const shapeStr = relativeData.shape.toLowerCase(); if (shapeStr.includes('circle')) normalizedShape = 'circle'; else if (shapeStr.includes('oval')) normalizedShape = 'oval'; else if (shapeStr.includes('roundrect')) normalizedShape = 'roundrect'; else if (shapeStr.includes('custom')) normalizedShape = 'custom'; return { center: { x: absoluteX, y: absoluteY }, shape: normalizedShape, type: normalizedType, size: { width: relativeData.size.width, height: relativeData.size.height }, rotation: absoluteRotation, layer: primaryLayer, layers: allLayers, number: relativeData.number }; } /** * Get all pad geometries for a component. * Useful for obstacle detection and collision checking. * * @param component - The component to get pads from * @returns Array of pad geometries */ static getAllPadGeometries(component) { const geometries = []; // Always parse the footprint and enumerate all pads to ensure // netless/mechanical pads are included as obstacles. This avoids // missing NC pads on footprints that also define electrical pins. try { const footprintSExpr = component.footprint_lib(component.footprint); const parsed = S.parse(footprintSExpr); if (!Array.isArray(parsed) || parsed.length === 0) { return geometries; } // Recursively collect all pad nodes const padNodes = []; const collectPads = (node) => { if (Array.isArray(node)) { if (node.length > 0 && String(node[0]) === 'pad') { padNodes.push(node); } for (const child of node) { collectPads(child); } } }; collectPads(parsed); for (const padNode of padNodes) { const rel = this.extractPadData(padNode); if (!rel) continue; const abs = this.transformToAbsolute(rel, component); if (abs) geometries.push(abs); } } catch { // Silently ignore if footprint cannot be parsed; no pads resolved } return geometries; } } // Exported as ESM for test/runtime compatibility