@typecad/typecad
Version:
🤖programmatically 💥create 🛰️hardware
276 lines (275 loc) • 13.4 kB
JavaScript
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;
}
}