UNPKG

@typecad/typecad

Version:

🤖programmatically 💥create 🛰️hardware

482 lines (481 loc) 20.7 kB
// Converted to ESM for compatibility with ESM test runners import { PadResolver } from './pad_resolver.js'; import chalk from 'chalk'; import SExpr from 's-expression.js'; import logger from '../../src/logging.js'; const S = new SExpr(); /** * Builds routing obstacles from PCB elements. * Converts components, pads, zones, and other PCB features into obstacle representations * for the routing grid. */ export class ObstacleBuilder { /** * Build all obstacles from a PCB instance. * * @param pcb - The PCB to extract obstacles from * @param defaultClearance - Default clearance for obstacles in mm * @param additionalComponents - Additional components to include (e.g., from pins being routed) * @returns Array of routing obstacles * * @example * ```ts * const obstacles = ObstacleBuilder.buildFromPCB(pcb, 0.2); * obstacles.forEach(obs => grid.addObstacle(obs)); * ``` */ static buildFromPCB(pcb, defaultClearance = 0.2, additionalComponents) { const obstacles = []; logger.debug(chalk.blue(`[ObstacleBuilder] Building obstacles from PCB`)); // Get components from PCB (access private fields through any for MVP) // Collect from both #components and #stagedComponents const allComponents = []; // Prefer internal getters to avoid JS private field access issues const components = pcb._getComponents?.() || pcb['#components']; const stagedComponents = pcb._getStagedComponents?.() || pcb['#stagedComponents']; if (components) { allComponents.push(...components); } if (stagedComponents) { allComponents.push(...stagedComponents); } // Add any additional components (e.g., from pins being routed) if (additionalComponents) { allComponents.push(...additionalComponents); } if (allComponents.length > 0) { logger.debug(chalk.blue(`[ObstacleBuilder] Processing ${allComponents.length} components (${components?.length || 0} active, ${stagedComponents?.length || 0} staged, ${additionalComponents?.length || 0} from pins)`)); let failedComponents = 0; const failedRefs = []; for (const component of allComponents) { if (component.dnp) { continue; // Skip DNP components } // Treat placed vias as circular pad obstacles so traces respect via-to-trace clearance if (component.via === true && component.viaData) { try { const viaObs = this.buildFromVia(component, defaultClearance); if (viaObs) { obstacles.push(viaObs); } continue; // Via handled, skip normal component pad processing } catch (e) { logger.warn(chalk.yellow(`[ObstacleBuilder] Error processing via obstacle: ${e?.message || e}`)); continue; } } try { // Add pad obstacles with net-aware collision detection // Pads block routes on different nets but not on same net const padObstacles = this.buildFromComponentPads(component, defaultClearance, pcb); obstacles.push(...padObstacles); } catch (error) { failedComponents++; failedRefs.push(component.reference); logger.warn(chalk.yellow(`[ObstacleBuilder] Error processing component ${component.reference}: ${error.message}`)); } } if (failedComponents > 0) { logger.warn(chalk.yellow(`[ObstacleBuilder] Failed to process ${failedComponents}/${allComponents.length} components: ${failedRefs.join(', ')}`)); } } else { logger.debug(chalk.yellow(`[ObstacleBuilder] No components found in PCB`)); } // Add zones const zones = pcb['#zones']; if (zones) { logger.debug(chalk.blue(`[ObstacleBuilder] Processing ${zones.length} filled zones`)); for (const zone of zones) { const zoneObstacle = this.buildFromZone(zone); if (zoneObstacle) { obstacles.push(zoneObstacle); } } } // Add keepout zones const keepoutZones = pcb._getKeepoutZones?.() || pcb['#keepoutZones']; if (keepoutZones) { logger.debug(chalk.blue(`[ObstacleBuilder] Processing ${keepoutZones.length} keepout zones`)); for (const keepout of keepoutZones) { const keepoutObstacle = this.buildFromKeepoutZone(keepout); if (keepoutObstacle) { obstacles.push(keepoutObstacle); } } } // Add board outlines as keepout const outlines = pcb.outlines; if (outlines && outlines.length > 0) { logger.debug(chalk.blue(`[ObstacleBuilder] Processing ${outlines.length} board outlines`)); for (const outline of outlines) { const outlineObstacle = this.buildFromOutline(outline, defaultClearance); if (outlineObstacle) { obstacles.push(outlineObstacle); } } } // Add existing tracks as obstacles (from committed tracks) const grLines = pcb['#grLines']; if (grLines && grLines.length > 0) { logger.debug(chalk.blue(`[ObstacleBuilder] Processing ${grLines.length} committed tracks`)); for (const line of grLines) { // Only process copper layer tracks if (line.layer.endsWith('.Cu')) { // Preserve net info so same-net routes can branch/merge correctly const trackObstacle = this.buildFromTrack(line, line.strokeWidth, defaultClearance, line.net); obstacles.push(trackObstacle); } } } // Add staged tracks as obstacles (tracks created but not yet committed via create()) const stagedOutlines = pcb._getStagedOutlines(); if (stagedOutlines && stagedOutlines.length > 0) { let stagedTrackCount = 0; for (const outline of stagedOutlines) { if (outline.elements) { for (const element of outline.elements) { if (element.type === 'line' && element.layer.endsWith('.Cu')) { const line = element; const trackObstacle = this.buildFromTrack(line, line.strokeWidth, defaultClearance, line.net); obstacles.push(trackObstacle); stagedTrackCount++; } } } } if (stagedTrackCount > 0) { logger.debug(chalk.blue(`[ObstacleBuilder] Added ${stagedTrackCount} staged track obstacles from previous autoroute calls`)); } } logger.debug(chalk.green(`[ObstacleBuilder] Built ${obstacles.length} total obstacles`)); return obstacles; } /** * Build an obstacle from a component's body (courtyard). * * @param component - The component * @param clearance - Clearance around component in mm * @returns Component body obstacle, or null if unable to determine bounds */ static buildFromComponent(component, clearance) { try { // Parse footprint to get courtyard bounds const footprintSExpr = component.footprint_lib(component.footprint); const parsed = S.parse(footprintSExpr); // Extract courtyard or fabrication bounds const bounds = this.extractComponentBounds(parsed, component); if (!bounds) { // Fallback: use simple bounding box estimate return this.buildSimpleComponentObstacle(component, clearance); } // Determine which layer the component is on const layer = component.pcb.side === 'back' ? 'B.Cu' : 'F.Cu'; return { type: 'component', bounds, layers: [layer], clearance, priority: 1 }; } catch (error) { console.warn(chalk_1.default.yellow(`[ObstacleBuilder] Could not build obstacle for ${component.reference}: ${error.message}`)); return this.buildSimpleComponentObstacle(component, clearance); } } /** * Build obstacles from a component's pads. * * @param component - The component * @param clearance - Clearance around pads in mm * @param pcb - PCB instance to resolve nets * @returns Array of pad obstacles */ static buildFromComponentPads(component, clearance, pcb) { const obstacles = []; const pads = PadResolver.getAllPadGeometries(component); // Precompute (reference:pin) -> net map once per component to avoid repeated scans let pinNetMap = null; const schematic = pcb.Schematic; if (schematic && Array.isArray(schematic.Nodes)) { pinNetMap = new Map(); for (const schematicNode of schematic.Nodes) { const netName = schematicNode?.name; const nodes = schematicNode?.nodes; if (!netName || !Array.isArray(nodes)) continue; for (const p of nodes) { const key = `${p.reference}:${String(p.number)}`; if (!pinNetMap.has(key)) { pinNetMap.set(key, netName); } } } } for (const pad of pads) { // Resolve net for this pad from precomputed map let netName; if (pinNetMap) { netName = pinNetMap.get(`${component.reference}:${String(pad.number)}`); } // Log pad info for debugging const layerInfo = pad.type === 'thru_hole' ? `all layers (${pad.layers.join(', ')})` : pad.layer; // Pad-level debug info (logged via logger so it respects TYPECAD_DEBUG/TYPECAD_QUIET) logger.debug(chalk.gray(`[ObstacleBuilder] Pad ${component.reference}.${pad.number}: ${pad.type} on ${layerInfo}, net: ${netName || 'none'}`)); // Always create obstacles for pads, even without nets (unconnected pads still block routing) const obstacle = this.buildFromPad(pad, clearance, netName); if (obstacle) { obstacles.push(obstacle); } } return obstacles; } /** * Build an obstacle from a pad geometry. * @private */ static buildFromPad(pad, clearance, net) { // Calculate bounding box based on pad shape let halfWidth = pad.size.width / 2; let halfHeight = pad.size.height / 2; // For rotated pads, calculate axis-aligned bounding box if (pad.rotation !== 0) { const rotRad = (pad.rotation * Math.PI) / 180; const cos = Math.abs(Math.cos(rotRad)); const sin = Math.abs(Math.sin(rotRad)); halfWidth = (pad.size.width * cos + pad.size.height * sin) / 2; halfHeight = (pad.size.width * sin + pad.size.height * cos) / 2; } // For through-hole pads, use all layers they exist on (typically all copper layers) // For SMD pads, use only the specific layer(s) they're on const layers = pad.layers && pad.layers.length > 0 ? pad.layers : [pad.layer]; return { type: 'pad', bounds: { minX: pad.center.x - halfWidth, maxX: pad.center.x + halfWidth, minY: pad.center.y - halfHeight, maxY: pad.center.y + halfHeight }, layers, // Use all layers for through-hole pads clearance, net, // Net-aware: obstacles on same net don't block priority: 2, // Higher priority than component body padShape: { shape: (pad.shape === 'custom' ? 'rect' : pad.shape), center: { x: pad.center.x, y: pad.center.y }, width: pad.size.width, height: pad.size.height, rotation: pad.rotation || 0 } }; } /** * Build an obstacle from a filled zone. * * @param zone - The filled zone * @returns Zone obstacle, or null if invalid */ static buildFromZone(zone) { if (!zone.polygon || zone.polygon.length === 0) { return null; } // Calculate bounding box from polygon const bounds = this.calculatePolygonBounds(zone.polygon); return { type: 'zone', bounds, layers: zone.layers, net: zone.net, clearance: zone.clearance || 0.2, priority: 0, // Lower priority - zones can often be routed over polygon: { points: zone.polygon } }; } /** * Build an obstacle from a keepout zone. * * @param zone - The keepout zone * @returns Keepout obstacle, or null if tracks are allowed */ static buildFromKeepoutZone(zone) { // Check if tracks are restricted in this keepout if (zone.restrictions?.tracks === false) { return null; // Tracks are allowed, no obstacle } if (!zone.polygon || zone.polygon.length === 0) { return null; } const bounds = this.calculatePolygonBounds(zone.polygon); return { type: 'keepout', bounds, layers: zone.layers, clearance: 0, // Keepout zones are absolute priority: 10, // Very high priority - absolute no-go zones polygon: { points: zone.polygon } }; } /** * Build an obstacle from a track. * * @param track - The track (IGrLine) * @param width - Track width in mm * @param clearance - Clearance around track in mm * @param net - Net name for net-aware routing (optional) * @returns Track obstacle */ static buildFromTrack(track, width, clearance, net, isManualRoute) { // Calculate bounding box for the track segment const minX = Math.min(track.start.x, track.end.x) - width / 2; const maxX = Math.max(track.start.x, track.end.x) + width / 2; const minY = Math.min(track.start.y, track.end.y) - width / 2; const maxY = Math.max(track.start.y, track.end.y) + width / 2; return { type: 'track', bounds: { minX, maxX, minY, maxY }, layers: [track.layer], clearance, net, // Net-aware: tracks on same net don't block (unless isManualRoute is true) priority: 1, isManualRoute, // Manual routes block even on same net segment: { x1: track.start.x, y1: track.start.y, x2: track.end.x, y2: track.end.y, width } }; } /** * Build an obstacle from a board outline. * * @param outline - The board outline * @param clearance - Clearance from outline in mm * @returns Outline obstacle (everything outside the outline is blocked) */ static buildFromOutline(outline, clearance) { // For now, treat outline as a simple bounding box keepout // TODO: Implement proper polygon-based outline handling return { type: 'outline', bounds: { minX: outline.x, maxX: outline.x + outline.width, minY: outline.y, maxY: outline.y + outline.height }, layers: ['F.Cu', 'B.Cu'], // Affects all copper layers clearance, priority: 10 // Very high priority }; } /** * Build an obstacle from a plated through via component. * Represent as a circular pad on all copper layers to enforce * trace-to-via clearances during routing. */ static buildFromVia(component, clearance) { const vd = component?.viaData; if (!vd || typeof vd.at?.x !== 'number' || typeof vd.at?.y !== 'number') { return null; } const size = typeof vd.size === 'number' ? vd.size : 0.6; // default via size const center = { x: vd.at.x, y: vd.at.y }; const half = size / 2; // Use declared layers if provided, otherwise affect all copper layers const layers = Array.isArray(vd.layers) && vd.layers.length > 0 ? vd.layers : ['F.Cu', 'B.Cu']; return { type: 'pad', bounds: { minX: center.x - half, maxX: center.x + half, minY: center.y - half, maxY: center.y + half, }, layers, clearance, priority: 1, padShape: { shape: 'circle', center, width: size, height: size, rotation: 0, }, }; } /** * Extract component bounds from footprint S-expression. * Looks for courtyard or fabrication layer bounds. * @private */ static extractComponentBounds(footprintSExpr, component) { // This is a simplified version - real implementation would parse // courtyard polygons from the footprint // For MVP, we'll use a simple approach: get all pads and create bounding box const pads = pad_resolver_1.PadResolver.getAllPadGeometries(component); if (pads.length === 0) { return null; } let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity; for (const pad of pads) { const halfWidth = pad.size.width / 2; const halfHeight = pad.size.height / 2; minX = Math.min(minX, pad.center.x - halfWidth); maxX = Math.max(maxX, pad.center.x + halfWidth); minY = Math.min(minY, pad.center.y - halfHeight); maxY = Math.max(maxY, pad.center.y + halfHeight); } // Expand bounds slightly to include component body const expansion = 1.0; // 1mm expansion around pads return { minX: minX - expansion, maxX: maxX + expansion, minY: minY - expansion, maxY: maxY + expansion }; } /** * Build a simple component obstacle using estimated dimensions. * @private */ static buildSimpleComponentObstacle(component, clearance) { // Estimate component size as 5mm x 5mm for unknown footprints const estimatedSize = 5.0; const layer = component.pcb.side === 'back' ? 'B.Cu' : 'F.Cu'; return { type: 'component', bounds: { minX: component.pcb.x - estimatedSize / 2, maxX: component.pcb.x + estimatedSize / 2, minY: component.pcb.y - estimatedSize / 2, maxY: component.pcb.y + estimatedSize / 2 }, layers: [layer], clearance, priority: 1 }; } /** * Calculate bounding box from a polygon. * @private */ static calculatePolygonBounds(polygon) { let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity; for (const point of polygon) { minX = Math.min(minX, point.x); maxX = Math.max(maxX, point.x); minY = Math.min(minY, point.y); maxY = Math.max(maxY, point.y); } return { minX, maxX, minY, maxY }; } } // exported above