UNPKG

@typecad/typecad

Version:

🤖programmatically 💥create 🛰️hardware

276 lines (275 loc) 13.4 kB
import chalk from 'chalk'; export class TrackBuilder { constructor(pcb) { this.currentLayer = "F.Cu"; // Default layer this.currentWidth = 0.2; // Default track width this.elements = []; this.lastOperationSuccessful = true; this.pcb = pcb; } from(startPos, layer, width) { if (!this.lastOperationSuccessful) return this; this.currentPosition = { x: startPos.x, y: startPos.y }; this.currentLayer = layer ?? this.currentLayer; this.currentWidth = width ?? this.currentWidth; return this; } powerInfo(info) { // console.log(`Setting power info: ${info.current}A, ${info.voltage}V, ${info.maxTempRise}°C`); this._powerInfo = { ...info, maxTempRise: info.maxTempRise ?? 10, // Default to 10 if not specified thickness: info.thickness ?? this.pcb.copper_thickness // Default to PCB copper thickness if not specified }; return this; } calculateMinTrackWidth(current, layer, maxTempRise, thickness) { // Use PCB copper thickness as default if not provided const actualThickness = thickness ?? this.pcb.copper_thickness; // IPC-2221 formula // Area[mils^2] = (Current[Amps]/(k*(Temp_Rise[deg. C])^b))^(1/c) // Width[mils] = Area[mils^2]/(Thickness[oz]*1.378[mils/oz]) // Width[mm] = Width[mils] * 0.0254 // Constants for IPC-2221 const k = (layer === "F.Cu" || layer === "B.Cu") ? 0.048 : 0.024; // external vs internal const b = 0.44; const c = 0.725; const milsPerOz = 1.378; // Convert thickness from microns to oz const thicknessOz = actualThickness / 35; // 35 microns = 1 oz // Calculate area in mils² const area = Math.pow(current / (k * Math.pow(maxTempRise, b)), 1 / c); // Calculate width in mils const widthMils = area / (thicknessOz * milsPerOz); // Convert to mm const widthMm = widthMils * 0.0254; // console.log(`Track calculation for ${current}A on ${layer}:`); // console.log(` - Temperature rise: ${maxTempRise}°C`); // console.log(` - Copper thickness: ${actualThickness}μm (${thicknessOz.toFixed(2)}oz)`); // console.log(` - Cross-sectional area: ${area.toFixed(2)} mils²`); // console.log(` - Width: ${widthMils.toFixed(2)} mils`); // console.log(` - Width: ${widthMm.toFixed(3)} mm`); return widthMm; } to(endPos) { if (!this.lastOperationSuccessful) { const callSite = this.getCallSite(); const callSiteInfo = callSite ? ` (called from ${callSite.file}:${callSite.line})` : ''; throw new Error(`Previous track operation failed${callSiteInfo}`); } if (!this.currentPosition) { const callSite = this.getCallSite(); const callSiteInfo = callSite ? ` (called from ${callSite.file}:${callSite.line})` : ''; throw new Error(`'from()' must be called before 'to()'${callSiteInfo}`); } const targetLayer = endPos.layer ?? this.currentLayer; const targetWidth = endPos.width ?? this.currentWidth; // Calculate track length const dx = endPos.x - this.currentPosition.x; const dy = endPos.y - this.currentPosition.y; const length = Math.sqrt(dx * dx + dy * dy); // console.log(`Creating track segment:`); // console.log(` - From: (${this.currentPosition.x.toFixed(3)}, ${this.currentPosition.y.toFixed(3)})`); // console.log(` - To: (${endPos.x.toFixed(3)}, ${endPos.y.toFixed(3)})`); // console.log(` - Length: ${length.toFixed(3)}mm`); // console.log(` - Width: ${targetWidth}mm`); // console.log(` - Layer: ${targetLayer}`); // If power info exists, check if width is sufficient if (this._powerInfo) { // console.log(`Checking power requirements for ${this._powerInfo.current}A`); const minWidth = this.calculateMinTrackWidth(this._powerInfo.current, targetLayer, this._powerInfo.maxTempRise, this._powerInfo.thickness); // Round minimum width to 3 decimal places const roundedMinWidth = Math.round(minWidth * 1000) / 1000; // console.log(`Width comparison: ${targetWidth}mm vs ${roundedMinWidth}mm (min)`); if (targetWidth < roundedMinWidth) { const callSite = this.getCallSite(); const callSiteInfo = callSite ? ` (called from ${callSite.file}:${callSite.line})` : ''; const error = `Track width ${targetWidth}mm is too narrow for ${this._powerInfo.current}A current on ${targetLayer}. Minimum width should be ${roundedMinWidth.toFixed(3)}mm.${callSiteInfo}`; console.error(chalk.red(`[TrackBuilder] ERROR: ${error}`)); // throw new Error(error); } } const persistentTrackUuid = this.pcb._track(this.currentPosition, { x: endPos.x, y: endPos.y }, targetWidth, targetLayer, false); // Get the actual track data from the PCB to include any preserved manual edits const actualTrackData = this.pcb._getTrackData(persistentTrackUuid); this.elements.push({ type: 'track', uuid: persistentTrackUuid, details: { start: { ...this.currentPosition }, end: { x: endPos.x, y: endPos.y }, width: actualTrackData ? actualTrackData.strokeWidth : targetWidth, layer: actualTrackData ? actualTrackData.layer : targetLayer, locked: actualTrackData?.locked ?? false, powerInfo: this._powerInfo } }); this.currentPosition = { x: endPos.x, y: endPos.y }; this.currentLayer = targetLayer; this.currentWidth = targetWidth; return this; } getCallSite() { const stack = new Error().stack; if (!stack) return null; const lines = stack.split('\n'); // Look for the first call that's NOT from this library // Skip Error constructor (line 0), this function (line 1), and the calling method (line 2) for (let i = 3; i < lines.length; i++) { const line = lines[i]; if (line) { const match = line.match(/at\s+(?:(.+?)\s+\()?(.+):(\d+):(\d+)\)?/); if (match) { const filePath = match[2]; // Skip internal library files - be more aggressive about excluding library code if (!filePath.includes('pcb_track_builder') && !filePath.includes('pcb.ts') && !filePath.includes('pcb.js') && !filePath.includes('node_modules') && !filePath.includes('\\dist\\') && !filePath.includes('/dist/')) { return { function: match[1]?.trim() || 'anonymous', file: filePath, line: parseInt(match[3], 10), column: parseInt(match[4], 10) }; } } } } // Fallback: if we can't find user code, return the immediate caller if (lines[2]) { const match = lines[2].match(/at\s+(?:(.+?)\s+\()?(.+):(\d+):(\d+)\)?/); if (match) { return { function: match[1]?.trim() || 'anonymous', file: match[2], line: parseInt(match[3], 10), column: parseInt(match[4], 10) }; } } return null; } calculateMinViaSize(current, thickness) { // Use PCB copper thickness as default if not provided const actualThickness = thickness ?? this.pcb.copper_thickness; // IPC-2221 formula for via current capacity // Similar to track width calculation but for circular cross-section // Area[mils^2] = (Current[Amps]/(k*(Temp_Rise[deg. C])^b))^(1/c) // where k = 0.048 for external layers (via barrel) const k = 0.048; // Via barrel is like external layer const b = 0.44; const c = 0.725; const milsPerOz = 1.378; // Convert thickness from microns to oz const thicknessOz = actualThickness / 35; // 35 microns = 1 oz // Calculate required cross-sectional area const area = Math.pow(current / (k * Math.pow(10, b)), 1 / c); // Using 10°C default temp rise // Calculate minimum drill diameter (in mils) // Area = π * (d/2)² // d = 2 * sqrt(Area/π) const drillMils = 2 * Math.sqrt(area / Math.PI); // Convert to mm const drillMm = drillMils * 0.0254; // Calculate annular ring from via size and drill // annularRing = (size - drill)/2 const annularRing = 0.1; // 0.1mm annular ring const sizeMm = drillMm + (2 * annularRing); console.log(`Via calculation for ${current}A:`); console.log(` - Drill: ${drillMm.toFixed(3)}mm`); console.log(` - Annular ring: ${annularRing}mm`); console.log(` - Total size: ${sizeMm.toFixed(3)}mm`); return { size: sizeMm, drill: drillMm }; } via(params = {}) { if (!this.lastOperationSuccessful) { const callSite = this.getCallSite(); const callSiteInfo = callSite ? ` (called from ${callSite.file}:${callSite.line})` : ''; throw new Error(`Previous track operation failed${callSiteInfo}`); } if (!this.currentPosition) { const callSite = this.getCallSite(); const callSiteInfo = callSite ? ` (called from ${callSite.file}:${callSite.line})` : ''; throw new Error(`'from()' must be called before 'via()'${callSiteInfo}`); } // If power info exists, calculate minimum via size if (params.powerInfo) { const minViaSize = this.calculateMinViaSize(params.powerInfo.current, params.powerInfo.thickness ?? this.pcb.copper_thickness); // Round to 3 decimal places const roundedSize = Math.round(minViaSize.size * 1000) / 1000; const roundedDrill = Math.round(minViaSize.drill * 1000) / 1000; if (!params.size || params.size < roundedSize) { params.size = roundedSize; } if (!params.drill || params.drill < roundedDrill) { params.drill = roundedDrill; } } const viaComponent = this.pcb.via({ at: this.currentPosition, size: params.size, drill: params.drill, }); if (viaComponent.viaData) { if (params.layers && params.layers.length > 0) { viaComponent.viaData.layers = params.layers; } else { let partnerLayer = "B.Cu"; if (this.currentLayer === "B.Cu") { partnerLayer = "F.Cu"; } else if (this.currentLayer !== "F.Cu") { partnerLayer = "F.Cu"; } viaComponent.viaData.layers = [this.currentLayer, partnerLayer]; if (viaComponent.viaData.layers[0] === viaComponent.viaData.layers[1]) { viaComponent.viaData.layers = ["F.Cu", "B.Cu"]; } } if (params.net) { viaComponent.viaData.net = params.net; } this.pcb.place(viaComponent); this.elements.push({ type: 'via', uuid: viaComponent.uuid, details: { ...viaComponent.viaData, powerInfo: params.powerInfo } }); const viaActualLayers = viaComponent.viaData.layers; if (viaActualLayers.length > 0) { if (viaActualLayers.includes(this.currentLayer) && viaActualLayers.length > 1) { this.currentLayer = viaActualLayers.find((l) => l !== this.currentLayer) || viaActualLayers[0]; } else { this.currentLayer = viaActualLayers[0]; } } } else { const callSite = this.getCallSite(); const callSiteInfo = callSite ? ` (called from ${callSite.file}:${callSite.line})` : ''; console.warn(chalk.yellow(`[TrackBuilder] WARN: 'via()' creation on PCB failed or returned unexpected structure. Via not added to track elements.${callSiteInfo}`)); this.lastOperationSuccessful = false; } return this; } getElements() { if (!this.lastOperationSuccessful) { const callSite = this.getCallSite(); const callSiteInfo = callSite ? ` (called from ${callSite.file}:${callSite.line})` : ''; console.warn(chalk.yellow(`[TrackBuilder] WARN: getElements() called on a builder chain that had a failing operation. Results might be incomplete.${callSiteInfo}`)); } return this.elements; } }